From c5b93f84038290fde1da235339fd5fdc67386876 Mon Sep 17 00:00:00 2001 From: Equbuxu Date: Fri, 3 Nov 2023 03:30:37 +0300 Subject: [PATCH 1/2] OkHsv/OkHsl wip --- .../ColorPicker.Models.csproj | 3 + .../ColorSliders/ColorSliderGradientPoint.cs | 28 + .../ColorSliders/ColorSliderType.cs | 25 + .../ColorSliders/ColorSliderTypeFactory.cs | 46 ++ .../ColorSliders/IColorSliderType.cs | 10 + .../Types/AlphaColorSliderType.cs | 17 + .../Types/HslLightnessColorSliderType.cs | 21 + .../Types/HslSaturationColorSliderType.cs | 18 + .../Types/HsvHslHueColorSliderType.cs | 29 + .../Types/HsvSaturationColorSliderType.cs | 18 + .../Types/HsvValueColorSliderType.cs | 18 + .../Types/OkHslHueColorSliderType.cs | 30 + .../Types/OkHslLightnessColorSliderType.cs | 21 + .../Types/OkHslSaturationColorSliderType.cs | 18 + .../Types/OkHsvHueColorSliderType.cs | 30 + .../Types/OkHsvSaturationColorSliderType.cs | 18 + .../Types/OkHsvValueColorSliderType.cs | 18 + .../Types/RgbBlueColorSliderType.cs | 17 + .../Types/RgbGreenColorSliderType.cs | 17 + .../Types/RgbRedColorSliderType.cs | 17 + src/ColorPicker.Models/ColorSpaceHelper.cs | 203 ------ .../ColorSpaces/HslHelper.cs | 95 +++ .../ColorSpaces/HsvHelper.cs | 93 +++ .../ColorSpaces/OkHelper.cs | 630 ++++++++++++++++++ .../ColorSpaces/OkHslHelper.cs | 58 ++ .../ColorSpaces/OkHsvHelper.cs | 58 ++ .../ColorSpaces/RgbHelper.cs | 103 +++ src/ColorPicker.Models/ColorState.cs | 268 +++++++- src/ColorPicker.Models/NotifyableColor.cs | 86 +++ src/ColorPicker.Models/PickerType.cs | 4 +- src/ColorPicker/AlphaSlider.xaml | 5 +- .../PreviewColorSlider.cs | 94 ++- src/ColorPicker/ColorSliders.xaml | 180 ++--- src/ColorPicker/DualPickerControlBase.cs | 4 +- .../Images/CircularHueGradientOkhsl.png | Bin 0 -> 33736 bytes .../Images/CircularHueGradientOkhsv.png | Bin 0 -> 23818 bytes src/ColorPicker/PickerControlBase.cs | 2 +- src/ColorPicker/SquarePicker.xaml | 33 +- src/ColorPicker/StandardColorPicker.xaml | 2 + src/ColorPicker/Styles/ColorSliderStyle.xaml | 13 +- .../UIExtensions/HslColorSlider.cs | 100 --- .../UIExtensions/HsvColorSlider.cs | 85 --- .../UIExtensions/RgbColorSlider.cs | 49 -- .../UserControls/SquareSlider.xaml.cs | 29 +- 44 files changed, 2033 insertions(+), 580 deletions(-) create mode 100644 src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs create mode 100644 src/ColorPicker.Models/ColorSliders/ColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/ColorSliderTypeFactory.cs create mode 100644 src/ColorPicker.Models/ColorSliders/IColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/AlphaColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/HslLightnessColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/HslSaturationColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/HsvHslHueColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/HsvSaturationColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/HsvValueColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/OkHslHueColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/OkHslLightnessColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/OkHslSaturationColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/OkHsvHueColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/OkHsvSaturationColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/OkHsvValueColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/RgbBlueColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/RgbGreenColorSliderType.cs create mode 100644 src/ColorPicker.Models/ColorSliders/Types/RgbRedColorSliderType.cs delete mode 100644 src/ColorPicker.Models/ColorSpaceHelper.cs create mode 100644 src/ColorPicker.Models/ColorSpaces/HslHelper.cs create mode 100644 src/ColorPicker.Models/ColorSpaces/HsvHelper.cs create mode 100644 src/ColorPicker.Models/ColorSpaces/OkHelper.cs create mode 100644 src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs create mode 100644 src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs create mode 100644 src/ColorPicker.Models/ColorSpaces/RgbHelper.cs rename src/ColorPicker/{UIExtensions => ColorSlider}/PreviewColorSlider.cs (51%) create mode 100644 src/ColorPicker/Images/CircularHueGradientOkhsl.png create mode 100644 src/ColorPicker/Images/CircularHueGradientOkhsv.png delete mode 100644 src/ColorPicker/UIExtensions/HslColorSlider.cs delete mode 100644 src/ColorPicker/UIExtensions/HsvColorSlider.cs delete mode 100644 src/ColorPicker/UIExtensions/RgbColorSlider.cs diff --git a/src/ColorPicker.Models/ColorPicker.Models.csproj b/src/ColorPicker.Models/ColorPicker.Models.csproj index 5c03cc9..7a90dcc 100644 --- a/src/ColorPicker.Models/ColorPicker.Models.csproj +++ b/src/ColorPicker.Models/ColorPicker.Models.csproj @@ -30,4 +30,7 @@ + + + diff --git a/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs b/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs new file mode 100644 index 0000000..0d2f85e --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs @@ -0,0 +1,28 @@ +using System; + +namespace ColorPicker.Models.ColorSliders; + +public struct ColorSliderGradientPoint +{ + public double R; + public double G; + public double B; + public double A = 1.0; + public double Position; + + public ColorSliderGradientPoint(double r, double g, double b, double position) + { + R = r; + G = g; + B = b; + Position = position; + } + + public ColorSliderGradientPoint(Tuple rgb, double position) + { + R = rgb.Item1; + G = rgb.Item2; + B = rgb.Item3; + Position = position; + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/ColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/ColorSliderType.cs new file mode 100644 index 0000000..2430fe6 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/ColorSliderType.cs @@ -0,0 +1,25 @@ +namespace ColorPicker.Models.ColorSliders; + +public enum ColorSliderType +{ + RgbRed, + RgbGreen, + RgbBlue, + Alpha, + + HsvHslHue, + + HsvSaturation, + HsvValue, + + HslSaturation, + HslLightness, + + OkHsvHue, + OkHsvSaturation, + OkHsvValue, + + OkHslHue, + OkHslSaturation, + OkHslLightness, +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/ColorSliderTypeFactory.cs b/src/ColorPicker.Models/ColorSliders/ColorSliderTypeFactory.cs new file mode 100644 index 0000000..8815fdd --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/ColorSliderTypeFactory.cs @@ -0,0 +1,46 @@ +using System; +using ColorPicker.Models.ColorSliders.Types; + +namespace ColorPicker.Models.ColorSliders; + +public static class ColorSliderTypeFactory +{ + public static IColorSliderType Get(ColorSliderType type) + { + switch (type) + { + case ColorSliderType.RgbRed: + return new RgbRedColorSliderType(); + case ColorSliderType.RgbGreen: + return new RgbGreenColorSliderType(); + case ColorSliderType.RgbBlue: + return new RgbBlueColorSliderType(); + case ColorSliderType.Alpha: + return new AlphaColorSliderType(); + case ColorSliderType.HsvHslHue: + return new HsvHslHueColorSliderType(); + case ColorSliderType.HsvSaturation: + return new HsvSaturationColorSliderType(); + case ColorSliderType.HsvValue: + return new HsvValueColorSliderType(); + case ColorSliderType.HslSaturation: + return new HslSaturationColorSliderType(); + case ColorSliderType.HslLightness: + return new HslLightnessColorSliderType(); + case ColorSliderType.OkHsvHue: + return new OkHsvHueColorSliderType(); + case ColorSliderType.OkHsvSaturation: + return new OkHsvSaturationColorSliderType(); + case ColorSliderType.OkHsvValue: + return new OkHsvValueColorSliderType(); + case ColorSliderType.OkHslHue: + return new OkHslHueColorSliderType(); + case ColorSliderType.OkHslSaturation: + return new OkHslSaturationColorSliderType(); + case ColorSliderType.OkHslLightness: + return new OkHslLightnessColorSliderType(); + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/IColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/IColorSliderType.cs new file mode 100644 index 0000000..3b13398 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/IColorSliderType.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace ColorPicker.Models.ColorSliders; + +public interface IColorSliderType +{ + List CalculateRgbGradient(ColorState currentColorState); + bool RefreshGradient { get; } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/AlphaColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/AlphaColorSliderType.cs new file mode 100644 index 0000000..7601424 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/AlphaColorSliderType.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class AlphaColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(state.RGB_R, state.RGB_G, state.RGB_B, 0.0) { A = 0 }, + new ColorSliderGradientPoint(state.RGB_R, state.RGB_G, state.RGB_B, 1.0) { A = 1 } + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/HslLightnessColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/HslLightnessColorSliderType.cs new file mode 100644 index 0000000..d0664aa --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/HslLightnessColorSliderType.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class HslLightnessColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.HslToRgb(state.HSL_H, state.HSL_S, 0), 0), + new ColorSliderGradientPoint(RgbHelper.HslToRgb(state.HSL_H, state.HSL_S, 0.25), 0.25), + new ColorSliderGradientPoint(RgbHelper.HslToRgb(state.HSL_H, state.HSL_S, 0.5), 0.5), + new ColorSliderGradientPoint(RgbHelper.HslToRgb(state.HSL_H, state.HSL_S, 0.75), 0.75), + new ColorSliderGradientPoint(RgbHelper.HslToRgb(state.HSL_H, state.HSL_S, 1), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/HslSaturationColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/HslSaturationColorSliderType.cs new file mode 100644 index 0000000..14083dc --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/HslSaturationColorSliderType.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class HslSaturationColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.HslToRgb(state.HSL_H, 0, state.HSL_L), 0), + new ColorSliderGradientPoint(RgbHelper.HslToRgb(state.HSL_H, 1, state.HSL_L), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/HsvHslHueColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/HsvHslHueColorSliderType.cs new file mode 100644 index 0000000..7de8729 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/HsvHslHueColorSliderType.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class HsvHslHueColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + GetPointAtHue(0, 0), + GetPointAtHue(60, 1 / 6.0), + GetPointAtHue(120, 2 / 6.0), + GetPointAtHue(180, 0.5), + GetPointAtHue(240, 4 / 6.0), + GetPointAtHue(300, 5 / 6.0), + GetPointAtHue(0, 1) + }; + } + + private ColorSliderGradientPoint GetPointAtHue(int value, double position) + { + var rgbTuple = RgbHelper.HsvToRgb(value, 1.0, 1.0); + return new ColorSliderGradientPoint(rgbTuple, position); + } + + public bool RefreshGradient => false; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/HsvSaturationColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/HsvSaturationColorSliderType.cs new file mode 100644 index 0000000..71408eb --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/HsvSaturationColorSliderType.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class HsvSaturationColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.HsvToRgb(state.HSV_H, 0, state.HSV_V), 0), + new ColorSliderGradientPoint(RgbHelper.HsvToRgb(state.HSV_H, 1, state.HSV_V), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/HsvValueColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/HsvValueColorSliderType.cs new file mode 100644 index 0000000..a6d0dae --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/HsvValueColorSliderType.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class HsvValueColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.HsvToRgb(state.HSV_H, state.HSV_S, 0), 0), + new ColorSliderGradientPoint(RgbHelper.HsvToRgb(state.HSV_H, state.HSV_S, 1), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/OkHslHueColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/OkHslHueColorSliderType.cs new file mode 100644 index 0000000..8a71711 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/OkHslHueColorSliderType.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class OkHslHueColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + GetPointAtHue(0, 0), + GetPointAtHue(22, 22 / 360.0), + GetPointAtHue(51, 51 / 360.00), + GetPointAtHue(139, 139 / 360.0), + GetPointAtHue(199, 199 / 360.0), + GetPointAtHue(245, 245 / 360.0), + GetPointAtHue(280, 280 / 360.0), + GetPointAtHue(0, 1) + }; + } + + private ColorSliderGradientPoint GetPointAtHue(int value, double position) + { + var rgbTuple = RgbHelper.OkHslToRgb(value, 1.0, 0.62); + return new ColorSliderGradientPoint(rgbTuple, position); + } + + public bool RefreshGradient => false; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/OkHslLightnessColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/OkHslLightnessColorSliderType.cs new file mode 100644 index 0000000..d0828a8 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/OkHslLightnessColorSliderType.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class OkHslLightnessColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.OkHslToRgb(state.OKHSL_H, state.OKHSL_S, 0), 0), + new ColorSliderGradientPoint(RgbHelper.OkHslToRgb(state.OKHSL_H, state.OKHSL_S, 0.25), 0.25), + new ColorSliderGradientPoint(RgbHelper.OkHslToRgb(state.OKHSL_H, state.OKHSL_S, 0.50), 0.50), + new ColorSliderGradientPoint(RgbHelper.OkHslToRgb(state.OKHSL_H, state.OKHSL_S, 0.75), 0.75), + new ColorSliderGradientPoint(RgbHelper.OkHslToRgb(state.OKHSL_H, state.OKHSL_S, 1), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/OkHslSaturationColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/OkHslSaturationColorSliderType.cs new file mode 100644 index 0000000..e63d910 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/OkHslSaturationColorSliderType.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class OkHslSaturationColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.OkHslToRgb(state.OKHSL_H, 0, state.OKHSL_L), 0), + new ColorSliderGradientPoint(RgbHelper.OkHslToRgb(state.OKHSL_H, 1, state.OKHSL_L), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/OkHsvHueColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/OkHsvHueColorSliderType.cs new file mode 100644 index 0000000..cd6d655 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/OkHsvHueColorSliderType.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class OkHsvHueColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + GetPointAtHue(0, 0), + GetPointAtHue(22, 22 / 360.0), + GetPointAtHue(51, 51 / 360.00), + GetPointAtHue(139, 139 / 360.0), + GetPointAtHue(199, 199 / 360.0), + GetPointAtHue(245, 245 / 360.0), + GetPointAtHue(280, 280 / 360.0), + GetPointAtHue(0, 1) + }; + } + + private ColorSliderGradientPoint GetPointAtHue(int value, double position) + { + var rgbTuple = RgbHelper.OkHsvToRgb(value, 1.0, 1.0); + return new ColorSliderGradientPoint(rgbTuple, position); + } + + public bool RefreshGradient => false; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/OkHsvSaturationColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/OkHsvSaturationColorSliderType.cs new file mode 100644 index 0000000..5cdf78f --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/OkHsvSaturationColorSliderType.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class OkHsvSaturationColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.OkHsvToRgb(state.OKHSV_H, 0, state.OKHSV_V), 0), + new ColorSliderGradientPoint(RgbHelper.OkHsvToRgb(state.OKHSV_H, 1, state.OKHSV_V), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/OkHsvValueColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/OkHsvValueColorSliderType.cs new file mode 100644 index 0000000..940b80c --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/OkHsvValueColorSliderType.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class OkHsvValueColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(RgbHelper.OkHsvToRgb(state.OKHSV_H, state.OKHSV_S, 0), 0), + new ColorSliderGradientPoint(RgbHelper.OkHsvToRgb(state.OKHSV_H, state.OKHSV_S, 1), 1) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/RgbBlueColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/RgbBlueColorSliderType.cs new file mode 100644 index 0000000..2c5b89b --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/RgbBlueColorSliderType.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class RgbBlueColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(state.RGB_R, state.RGB_G, 0, 0.0), + new ColorSliderGradientPoint(state.RGB_R, state.RGB_G, 1, 1.0) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/RgbGreenColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/RgbGreenColorSliderType.cs new file mode 100644 index 0000000..445a534 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/RgbGreenColorSliderType.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class RgbGreenColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(state.RGB_R, 0, state.RGB_B, 0.0), + new ColorSliderGradientPoint(state.RGB_R, 1, state.RGB_B, 1.0) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSliders/Types/RgbRedColorSliderType.cs b/src/ColorPicker.Models/ColorSliders/Types/RgbRedColorSliderType.cs new file mode 100644 index 0000000..cd5aaee --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/Types/RgbRedColorSliderType.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace ColorPicker.Models.ColorSliders.Types; + +internal class RgbRedColorSliderType : IColorSliderType +{ + public List CalculateRgbGradient(ColorState state) + { + return new List() + { + new ColorSliderGradientPoint(0, state.RGB_G, state.RGB_B, 0.0), + new ColorSliderGradientPoint(1, state.RGB_G, state.RGB_B, 1.0) + }; + } + + public bool RefreshGradient => true; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaceHelper.cs b/src/ColorPicker.Models/ColorSpaceHelper.cs deleted file mode 100644 index 16eab7e..0000000 --- a/src/ColorPicker.Models/ColorSpaceHelper.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System; - -namespace ColorPicker.Models -{ - public static class ColorSpaceHelper - { - /// - /// Converts RGB to HSV, returns -1 for undefined channels - /// - /// Red channel - /// Green channel - /// Blue channel - /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Value (0-1) - public static Tuple RgbToHsv(double r, double g, double b) - { - double min, max, delta; - double h, s, v; - - min = Math.Min(r, Math.Min(g, b)); - max = Math.Max(r, Math.Max(g, b)); - v = max; - delta = max - min; - if (max != 0) - { - s = delta / max; - } - else - { - //pure black - s = -1; - h = -1; - return new Tuple(h, s, v); - } - - if (r == max) - h = (g - b) / delta; // between yellow & magenta - else if (g == max) - h = 2 + (b - r) / delta; // between cyan & yellow - else - h = 4 + (r - g) / delta; // between magenta & cyan - h *= 60; - if (h < 0) - h += 360; - if (double.IsNaN(h)) //delta == 0, case of pure gray - h = -1; - - return new Tuple(h, s, v); - } - - /// - /// Converts RGB to HSL, returns -1 for undefined channels - /// - /// Red channel - /// Blue channel - /// Green channel - /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Lightness (0-1) - public static Tuple RgbToHsl(double r, double g, double b) - { - double h, s, l; - - var min = Math.Min(Math.Min(r, g), b); - var max = Math.Max(Math.Max(r, g), b); - var delta = max - min; - l = (max + min) / 2; - - if (max == 0) - //pure black - return new Tuple(-1, -1, 0); - - if (delta == 0) - //gray - return new Tuple(-1, 0, l); - - //magic - s = l <= 0.5 ? delta / (max + min) : delta / (2 - max - min); - - if (r == max) - h = (g - b) / 6 / delta; - else if (g == max) - h = 1.0f / 3 + (b - r) / 6 / delta; - else - h = 2.0f / 3 + (r - g) / 6 / delta; - - if (h < 0) - h += 1; - if (h > 1) - h -= 1; - - h *= 360; - - return new Tuple(h, s, l); - } - - /// - /// Converts HSV to RGB - /// - /// Hue, 0-360 - /// Saturation, 0-1 - /// Value, 0-1 - /// Values (0-1) in order: R, G, B - public static Tuple HsvToRgb(double h, double s, double v) - { - if (s == 0) - // achromatic (grey) - return new Tuple(v, v, v); - if (h >= 360.0) - h = 0; - h /= 60; - var i = (int)h; - var f = h - i; - var p = v * (1 - s); - var q = v * (1 - s * f); - var t = v * (1 - s * (1 - f)); - - switch (i) - { - case 0: return new Tuple(v, t, p); - case 1: return new Tuple(q, v, p); - case 2: return new Tuple(p, v, t); - case 3: return new Tuple(p, q, v); - case 4: return new Tuple(t, p, v); - default: return new Tuple(v, p, q); - } - - ; - } - - /// - /// Converts HSV to HSL - /// - /// Hue, 0-360 - /// Saturation, 0-1 - /// Value, 0-1 - /// Values in order: Hue (same), Saturation (0-1 or -1), Lightness (0-1) - public static Tuple HsvToHsl(double h, double s, double v) - { - var hsl_l = v * (1 - s / 2); - double hsl_s; - if (hsl_l == 0 || hsl_l == 1) - hsl_s = -1; - else - hsl_s = (v - hsl_l) / Math.Min(hsl_l, 1 - hsl_l); - return new Tuple(h, hsl_s, hsl_l); - } - - /// - /// Converts HSL to RGB - /// - /// Hue, 0-360 - /// Saturation, 0-1 - /// Lightness, 0-1 - /// Values (0-1) in order: R, G, B - public static Tuple HslToRgb(double h, double s, double l) - { - var hueCircleSegment = (int)(h / 60); - var circleSegmentFraction = (h - 60 * hueCircleSegment) / 60; - - var maxRGB = l < 0.5 ? l * (1 + s) : l + s - l * s; - var minRGB = 2 * l - maxRGB; - var delta = maxRGB - minRGB; - - switch (hueCircleSegment) - { - case 0: - return new Tuple(maxRGB, delta * circleSegmentFraction + minRGB, - minRGB); //red-yellow - case 1: - return new Tuple(delta * (1 - circleSegmentFraction) + minRGB, maxRGB, - minRGB); //yellow-green - case 2: - return new Tuple(minRGB, maxRGB, - delta * circleSegmentFraction + minRGB); //green-cyan - case 3: - return new Tuple(minRGB, delta * (1 - circleSegmentFraction) + minRGB, - maxRGB); //cyan-blue - case 4: - return new Tuple(delta * circleSegmentFraction + minRGB, minRGB, - maxRGB); //blue-purple - default: - return new Tuple(maxRGB, minRGB, - delta * (1 - circleSegmentFraction) + minRGB); //purple-red and invalid values - } - } - - /// - /// Converts HSL to HSV - /// - /// Hue, 0-360 - /// Saturation, 0-1 - /// Lightness, 0-1 - /// Values in order: Hue (same), Saturation (0-1 or -1), Value (0-1) - public static Tuple HslToHsv(double h, double s, double l) - { - var hsv_v = l + s * Math.Min(l, 1 - l); - double hsv_s; - if (hsv_v == 0) - hsv_s = -1; - else - hsv_s = 2 * (1 - l / hsv_v); - return new Tuple(h, hsv_s, hsv_v); - } - } -} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/HslHelper.cs b/src/ColorPicker.Models/ColorSpaces/HslHelper.cs new file mode 100644 index 0000000..05768ae --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/HslHelper.cs @@ -0,0 +1,95 @@ +using System; + +namespace ColorPicker.Models.ColorSpaces; + +public static class HslHelper +{ + /// + /// Converts RGB to HSL, returns -1 for undefined channels + /// + /// Red channel + /// Blue channel + /// Green channel + /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Lightness (0-1) + public static Tuple RgbToHsl(double r, double g, double b) + { + double h, s, l; + + var min = Math.Min(Math.Min(r, g), b); + var max = Math.Max(Math.Max(r, g), b); + var delta = max - min; + l = (max + min) / 2; + + if (max == 0) + //pure black + return new Tuple(-1, -1, 0); + + if (delta == 0) + //gray + return new Tuple(-1, 0, l); + + //magic + s = l <= 0.5 ? delta / (max + min) : delta / (2 - max - min); + + if (r == max) + h = (g - b) / 6 / delta; + else if (g == max) + h = 1.0f / 3 + (b - r) / 6 / delta; + else + h = 2.0f / 3 + (r - g) / 6 / delta; + + if (h < 0) + h += 1; + if (h > 1) + h -= 1; + + h *= 360; + + return new Tuple(h, s, l); + } + + /// + /// Converts HSV to HSL + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values in order: Hue (same), Saturation (0-1 or -1), Lightness (0-1) + public static Tuple HsvToHsl(double h, double s, double v) + { + var hsl_l = v * (1 - s / 2); + double hsl_s; + if (hsl_l == 0 || hsl_l == 1) + hsl_s = -1; + else + hsl_s = (v - hsl_l) / Math.Min(hsl_l, 1 - hsl_l); + return new Tuple(h, hsl_s, hsl_l); + } + + + /// + /// Converts OKHSL to HSL + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) + public static Tuple OkHslToHsl(double h, double s, double l) + { + var rgb = RgbHelper.OkHslToRgb(h, s, l); + return HslHelper.RgbToHsl(rgb.Item1, rgb.Item2, rgb.Item3); + } + + /// + /// Converts OKHSV to HSL + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) + public static Tuple OkHsvToHsl(double h, double s, double v) + { + var rgb = RgbHelper.OkHsvToRgb(h, s, v); + return HslHelper.RgbToHsl(rgb.Item1, rgb.Item2, rgb.Item3); + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs b/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs new file mode 100644 index 0000000..1994dad --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs @@ -0,0 +1,93 @@ +using System; + +namespace ColorPicker.Models.ColorSpaces; + +public static class HsvHelper +{ + /// + /// Converts HSL to HSV + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values in order: Hue (same), Saturation (0-1 or -1), Value (0-1) + public static Tuple HslToHsv(double h, double s, double l) + { + var hsv_v = l + s * Math.Min(l, 1 - l); + double hsv_s; + if (hsv_v == 0) + hsv_s = -1; + else + hsv_s = 2 * (1 - l / hsv_v); + return new Tuple(h, hsv_s, hsv_v); + } + + /// + /// Converts RGB to HSV, returns -1 for undefined channels + /// + /// Red channel + /// Green channel + /// Blue channel + /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Value (0-1) + public static Tuple RgbToHsv(double r, double g, double b) + { + double min, max, delta; + double h, s, v; + + min = Math.Min(r, Math.Min(g, b)); + max = Math.Max(r, Math.Max(g, b)); + v = max; + delta = max - min; + if (max != 0) + { + s = delta / max; + } + else + { + //pure black + s = -1; + h = -1; + return new Tuple(h, s, v); + } + + if (r == max) + h = (g - b) / delta; // between yellow & magenta + else if (g == max) + h = 2 + (b - r) / delta; // between cyan & yellow + else + h = 4 + (r - g) / delta; // between magenta & cyan + h *= 60; + if (h < 0) + h += 360; + if (double.IsNaN(h)) //delta == 0, case of pure gray + h = -1; + + return new Tuple(h, s, v); + } + + /// + /// Converts OKHSL to HSV + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) + public static Tuple OkHslToHsv(double h, double s, double l) + { + var rgb = RgbHelper.OkHslToRgb(h, s, l); + return HsvHelper.RgbToHsv(rgb.Item1, rgb.Item2, rgb.Item3); + } + + /// + /// Converts OKHSV to HSV + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) + public static Tuple OkHsvToHsv(double h, double s, double v) + { + var rgb = RgbHelper.OkHsvToRgb(h, s, v); + return HsvHelper.RgbToHsv(rgb.Item1, rgb.Item2, rgb.Item3); + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/OkHelper.cs b/src/ColorPicker.Models/ColorSpaces/OkHelper.cs new file mode 100644 index 0000000..74105fb --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/OkHelper.cs @@ -0,0 +1,630 @@ +// Adapted from Björn Ottosson's C++ header https://bottosson.github.io/misc/ok_color.h +using System; + +namespace ColorPicker.Models.ColorSpaces +{ + public static class OkHelper + { + private struct Lab + { + public double L; + public double a; + public double b; + }; + + private struct RGB + { + public double r; + public double g; + public double b; + }; + + private struct HSV + { + public double h; + public double s; + public double v; + }; + + private struct HSL + { + public double h; + public double s; + public double l; + }; + + private struct LC + { + public double L; + public double C; + }; + + /// + /// Alternative representation of (L_cusp, C_cusp) + /// Encoded so S = C_cusp/L_cusp and T = C_cusp/(1-L_cusp) + /// The maximum value for C in the triangle is then found as fmin(S*L, T*(1-L)), for a given L + /// + private struct ST + { + public double S; + public double T; + }; + + private static double SrgbTransferFunction(double a) + { + return .0031308 >= a ? 12.92 * a : 1.055 * Math.Pow(a, .4166666666666667) - .055; + } + + private static double SrgbTransferFunctionInverse(double a) + { + return .04045 < a ? Math.Pow((a + .055) / 1.055, 2.4) : a / 12.92; + } + + private static Lab LinearSrgbToOklab(RGB c) + { + double l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b; + double m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b; + double s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b; + + double l_ = Math.Cbrt(l); + double m_ = Math.Cbrt(m); + double s_ = Math.Cbrt(s); + + return new Lab() + { + L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, + a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, + b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_ + }; + } + + private static RGB OklabToLinearSrgb(Lab c) + { + double l_ = c.L + 0.3963377774 * c.a + 0.2158037573 * c.b; + double m_ = c.L - 0.1055613458 * c.a - 0.0638541728 * c.b; + double s_ = c.L - 0.0894841775 * c.a - 1.2914855480 * c.b; + + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; + + return new RGB() + { + r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s + }; + } + + /// + /// Finds the maximum saturation possible for a given hue that fits in sRGB + /// Saturation here is defined as S = C/L + /// a and b must be normalized so a^2 + b^2 == 1 + /// + private static double ComputeMaxSaturation(double a, double b) + { + // Max saturation will be when one of r, g or b goes below zero. + + // Select different coefficients depending on which component goes below zero first + double k0, k1, k2, k3, k4, wl, wm, ws; + + if (-1.88170328 * a - 0.80936493 * b > 1) + { + // Red component + k0 = +1.19086277; + k1 = +1.76576728; + k2 = +0.59662641; + k3 = +0.75515197; + k4 = +0.56771245; + wl = +4.0767416621; + wm = -3.3077115913; + ws = +0.2309699292; + } + else if (1.81444104 * a - 1.19445276 * b > 1) + { + // Green component + k0 = +0.73956515; + k1 = -0.45954404; + k2 = +0.08285427; + k3 = +0.12541070; + k4 = +0.14503204; + wl = -1.2684380046; + wm = +2.6097574011; + ws = -0.3413193965; + } + else + { + // Blue component + k0 = +1.35733652; + k1 = -0.00915799; + k2 = -1.15130210; + k3 = -0.50559606; + k4 = +0.00692167; + wl = -0.0041960863; + wm = -0.7034186147; + ws = +1.7076147010; + } + + // Approximate max saturation using a polynomial: + double S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; + + // Do one step Halley's method to get closer + // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite + // this should be sufficient for most applications, otherwise do two/three steps + + double k_l = +0.3963377774 * a + 0.2158037573 * b; + double k_m = -0.1055613458 * a - 0.0638541728 * b; + double k_s = -0.0894841775 * a - 1.2914855480 * b; + + { + double l_ = 1.0 + S * k_l; + double m_ = 1.0 + S * k_m; + double s_ = 1.0 + S * k_s; + + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; + + double l_dS = 3.0 * k_l * l_ * l_; + double m_dS = 3.0 * k_m * m_ * m_; + double s_dS = 3.0 * k_s * s_ * s_; + + double l_dS2 = 6.0 * k_l * k_l * l_; + double m_dS2 = 6.0 * k_m * k_m * m_; + double s_dS2 = 6.0 * k_s * k_s * s_; + + double f = wl * l + wm * m + ws * s; + double f1 = wl * l_dS + wm * m_dS + ws * s_dS; + double f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; + + S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); + } + + return S; + } + + /// + /// finds L_cusp and C_cusp for a given hue + /// a and b must be normalized so a^2 + b^2 == 1 + /// + private static LC FindCusp(double a, double b) + { + // First, find the maximum saturation (saturation S = C/L) + double S_cusp = ComputeMaxSaturation(a, b); + + // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: + RGB rgb_at_max = OklabToLinearSrgb(new Lab() + { + L = 1, + a = S_cusp * a, + b = S_cusp * b + }); + + double L_cusp = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b)); + double C_cusp = L_cusp * S_cusp; + + return new LC + { + L = L_cusp, + C = C_cusp + }; + } + + /// + /// Finds intersection of the line defined by + /// L = L0 * (1 - t) + t * L1; + /// C = t * C1; + /// a and b must be normalized so a^2 + b^2 == 1 + /// + private static double FindGamutIntersection(double a, double b, double L1, double C1, double L0, LC cusp) + { + // Find the intersection for upper and lower half seprately + double t; + if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.0) + { + // Lower half + + t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); + } + else + { + // Upper half + + // First intersect with triangle + t = cusp.C * (L0 - 1.0) / (C1 * (cusp.L - 1.0) + cusp.C * (L0 - L1)); + + // Then one step Halley's method + { + double dL = L1 - L0; + double dC = C1; + + double k_l = +0.3963377774 * a + 0.2158037573 * b; + double k_m = -0.1055613458 * a - 0.0638541728 * b; + double k_s = -0.0894841775 * a - 1.2914855480 * b; + + double l_dt = dL + dC * k_l; + double m_dt = dL + dC * k_m; + double s_dt = dL + dC * k_s; + + + // If higher accuracy is required, 2 or 3 iterations of the following block can be used: + { + double L = L0 * (1.0 - t) + t * L1; + double C = t * C1; + + double l_ = L + C * k_l; + double m_ = L + C * k_m; + double s_ = L + C * k_s; + + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; + + double ldt = 3 * l_dt * l_ * l_; + double mdt = 3 * m_dt * m_ * m_; + double sdt = 3 * s_dt * s_ * s_; + + double ldt2 = 6 * l_dt * l_dt * l_; + double mdt2 = 6 * m_dt * m_dt * m_; + double sdt2 = 6 * s_dt * s_dt * s_; + + double r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1; + double r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; + double r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; + + double u_r = r1 / (r1 * r1 - 0.5 * r * r2); + double t_r = -r * u_r; + + double g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1; + double g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; + double g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; + + double u_g = g1 / (g1 * g1 - 0.5 * g * g2); + double t_g = -g * u_g; + + double _b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1; + double b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; + double b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; + + double u_b = b1 / (b1 * b1 - 0.5 * _b * b2); + double t_b = -_b * u_b; + + t_r = u_r >= 0.0 ? t_r : float.MaxValue; + t_g = u_g >= 0.0 ? t_g : float.MaxValue; + t_b = u_b >= 0.0 ? t_b : float.MaxValue; + + t += Math.Min(t_r, Math.Min(t_g, t_b)); + } + } + } + + return t; + } + + private static double FindGamutIntersection(double a, double b, double L1, double C1, double L0) + { + // Find the cusp of the gamut triangle + LC cusp = FindCusp(a, b); + + return FindGamutIntersection(a, b, L1, C1, L0, cusp); + } + + private static double Toe(double x) + { + const double k_1 = 0.206; + const double k_2 = 0.03; + const double k_3 = (1.0 + k_1) / (1.0 + k_2); + return 0.5 * (k_3 * x - k_1 + Math.Sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x)); + } + + private static double ToeInverse(double x) + { + const double k_1 = 0.206; + const double k_2 = 0.03; + const double k_3 = (1.0 + k_1) / (1.0 + k_2); + return (x * x + k_1 * x) / (k_3 * (x + k_2)); + } + + private static ST ToSt(LC cusp) + { + double L = cusp.L; + double C = cusp.C; + return new ST() + { + S = C / L, + T = C / (1 - L) + }; + } + + /// + /// Returns a smooth approximation of the location of the cusp + /// This polynomial was created by an optimization process + /// It has been designed so that S_mid < S_max and T_mid < T_max + /// + private static ST GetStMid(double a_, double b_) + { + double S = 0.11516993 + 1.0 / ( + +7.44778970 + 4.15901240 * b_ + + a_ * (-2.19557347 + 1.75198401 * b_ + + a_ * (-2.13704948 - 10.02301043 * b_ + + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_ + ))) + ); + + double T = 0.11239642 + 1.0 / ( + +1.61320320 - 0.68124379 * b_ + + a_ * (+0.40370612 + 0.90148123 * b_ + + a_ * (-0.27087943 + 0.61223990 * b_ + + a_ * (+0.00299215 - 0.45399568 * b_ - 0.14661872 * a_ + ))) + ); + + return new ST() + { + S = S, + T = T + }; + } + + struct Cs + { + public double C_0; + public double C_mid; + public double C_max; + }; + + private static Cs GetCs(double L, double a_, double b_) + { + LC cusp = FindCusp(a_, b_); + + double C_max = FindGamutIntersection(a_, b_, L, 1, L, cusp); + ST ST_max = ToSt(cusp); + + // Scale factor to compensate for the curved part of gamut shape: + double k = C_max / Math.Min((L * ST_max.S), (1 - L) * ST_max.T); + + double C_mid; + { + ST ST_mid = GetStMid(a_, b_); + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + double C_a = L * ST_mid.S; + double C_b = (1.0 - L) * ST_mid.T; + C_mid = 0.9 * k * Math.Sqrt(Math.Sqrt(1.0 / (1.0 / (C_a * C_a * C_a * C_a) + 1.0 / (C_b * C_b * C_b * C_b)))); + } + + double C_0; + { + // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST. + double C_a = L * 0.4; + double C_b = (1.0 - L) * 0.8; + + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + C_0 = Math.Sqrt(1.0 / (1.0 / (C_a * C_a) + 1.0 / (C_b * C_b))); + } + + return new Cs() + { + C_0 = C_0, + C_mid = C_mid, + C_max = C_max + }; + } + + public static Tuple OkHslToSrgb(double h, double s, double l) + { + if (l == 1.0) + { + return new Tuple(1.0, 1.0, 1.0); + } + else if (l == 0.0) + { + return new Tuple(0.0, 0.0, 0.0); + } + + double a_ = Math.Cos(2.0 * Math.PI * h); + double b_ = Math.Sin(2.0 * Math.PI * h); + double L = ToeInverse(l); + + Cs cs = GetCs(L, a_, b_); + double C_0 = cs.C_0; + double C_mid = cs.C_mid; + double C_max = cs.C_max; + + double mid = 0.8; + double mid_inv = 1.25; + + double C, t, k_0, k_1, k_2; + + if (s < mid) + { + t = mid_inv * s; + + k_1 = mid * C_0; + k_2 = (1.0 - k_1 / C_mid); + + C = t * k_1 / (1.0 - k_2 * t); + } + else + { + t = (s - mid) / (1 - mid); + + k_0 = C_mid; + k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + k_2 = (1.0 - (k_1) / (C_max - C_mid)); + + C = k_0 + t * k_1 / (1.0 - k_2 * t); + } + + RGB rgb = OklabToLinearSrgb(new Lab() + { + L = L, + a = C * a_, + b= C * b_ + }); + + return new Tuple( + SrgbTransferFunction(rgb.r), + SrgbTransferFunction(rgb.g), + SrgbTransferFunction(rgb.b) + ); + } + + public static Tuple SrgbToOkHsl(double r, double g, double b) + { + Lab lab = LinearSrgbToOklab(new RGB() + { + r = SrgbTransferFunctionInverse(r), + g = SrgbTransferFunctionInverse(g), + b = SrgbTransferFunctionInverse(b) + }); + + double C = Math.Sqrt(lab.a * lab.a + lab.b * lab.b); + double a_ = lab.a / C; + double b_ = lab.b / C; + + double L = lab.L; + double h = 0.5 + 0.5 * Math.Atan2(-lab.b, -lab.a) / Math.PI; + + Cs cs = GetCs(L, a_, b_); + double C_0 = cs.C_0; + double C_mid = cs.C_mid; + double C_max = cs.C_max; + + // Inverse of the interpolation in OkHslToSrgb: + + double mid = 0.8; + double mid_inv = 1.25; + + double s; + if (C < C_mid) + { + double k_1 = mid * C_0; + double k_2 = (1.0 - k_1 / C_mid); + + double t = C / (k_1 + k_2 * C); + s = t * mid; + } + else + { + double k_0 = C_mid; + double k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + double k_2 = (1.0 - (k_1) / (C_max - C_mid)); + + double t = (C - k_0) / (k_1 + k_2 * (C - k_0)); + s = mid + (1.0 - mid) * t; + } + + double l = Toe(L); + return new Tuple( + h, + s, + l + ); + } + + + public static Tuple OkHsvToSrgb(double h, double s, double v) + { + double a_ = Math.Cos(2.0 * Math.PI * h); + double b_ = Math.Sin(2.0 * Math.PI * h); + + LC cusp = FindCusp(a_, b_); + ST ST_max = ToSt(cusp); + double S_max = ST_max.S; + double T_max = ST_max.T; + double S_0 = 0.5; + double k = 1 - S_0 / S_max; + + // first we compute L and V as if the gamut is a perfect triangle: + + // L, C when v==1: + double L_v = 1 - s * S_0 / (S_0 + T_max - T_max * k * s); + double C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); + + double L = v * L_v; + double C = v * C_v; + + // then we compensate for both Toe and the curved top part of the triangle: + double L_vt = ToeInverse(L_v); + double C_vt = C_v * L_vt / L_v; + + double L_new = ToeInverse(L); + C = C * L_new / L; + L = L_new; + + RGB rgb_scale = OklabToLinearSrgb(new Lab() + { + L = L_vt, a = a_* C_vt, b = b_ *C_vt + }); + double scale_L = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_scale.r, rgb_scale.g), Math.Max(rgb_scale.b, 0.0))); + + L = L * scale_L; + C = C * scale_L; + + RGB rgb = OklabToLinearSrgb( new Lab() + { + L = L, a = C * a_, b = C * b_ + }); + + return new Tuple( + SrgbTransferFunction(rgb.r), + SrgbTransferFunction(rgb.g), + SrgbTransferFunction(rgb.b) + ); + } + + public static Tuple SrgbToOkHsv(double r, double g, double b) + { + Lab lab = LinearSrgbToOklab(new RGB() + { + r = SrgbTransferFunctionInverse(r), + g = SrgbTransferFunctionInverse(g), + b = SrgbTransferFunctionInverse(b) + }); + + double C = Math.Sqrt(lab.a * lab.a + lab.b * lab.b); + double a_ = lab.a / C; + double b_ = lab.b / C; + + double L = lab.L; + double h = 0.5 + 0.5 * Math.Atan2(-lab.b, -lab.a) / Math.PI; + + LC cusp = FindCusp(a_, b_); + ST ST_max = ToSt(cusp); + double S_max = ST_max.S; + double T_max = ST_max.T; + double S_0 = 0.5; + double k = 1 - S_0 / S_max; + + // first we find L_v, C_v, L_vt and C_vt + + double t = T_max / (C + L * T_max); + double L_v = t * L; + double C_v = t * C; + + double L_vt = ToeInverse(L_v); + double C_vt = C_v * L_vt / L_v; + + // we can then use these to invert the step that compensates for the Toe and the curved top part of the triangle: + RGB rgb_scale = OklabToLinearSrgb(new Lab() + { + L = L_vt, a = a_* C_vt, b = b_ *C_vt + }); + double scale_L = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_scale.r, rgb_scale.g), Math.Max(rgb_scale.b, 0.0))); + + L = L / scale_L; + C = C / scale_L; + + C = C * Toe(L) / L; + L = Toe(L); + + // we can now compute v and s: + + double v = L / L_v; + double s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); + + return new Tuple(h, s, v); + } + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs b/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs new file mode 100644 index 0000000..e70f4e9 --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs @@ -0,0 +1,58 @@ +using System; + +namespace ColorPicker.Models.ColorSpaces; + +public static class OkHslHelper +{ + /// + /// Converts RGB to OKHSL, returns -1 for undefined channels + /// + /// Red channel + /// Blue channel + /// Green channel + /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Lightness (0-1) + public static Tuple RgbToOkHsl(double r, double g, double b) + { + var tuple = OkHelper.SrgbToOkHsl(r, g, b); + return new Tuple(tuple.Item1 * 360, tuple.Item2, tuple.Item3); + } + + /// + /// Converts HSL to OKHSL + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) + public static Tuple HslToOkHsl(double h, double s, double l) + { + var rgb = RgbHelper.HslToRgb(h, s, l); + return OkHslHelper.RgbToOkHsl(rgb.Item1, rgb.Item2, rgb.Item3); + } + + /// + /// Converts HSV to OKHSL + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) + public static Tuple HsvToOkHsl(double h, double s, double v) + { + var rgb = RgbHelper.HsvToRgb(h, s, v); + return OkHslHelper.RgbToOkHsl(rgb.Item1, rgb.Item2, rgb.Item3); + } + + /// + /// Converts OKHSV to OKHSL + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) + public static Tuple OkHsvToOkHsl(double h, double s, double v) + { + var rgb = RgbHelper.OkHsvToRgb(h, s, v); + return OkHslHelper.RgbToOkHsl(rgb.Item1, rgb.Item2, rgb.Item3); + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs b/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs new file mode 100644 index 0000000..fc9e122 --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs @@ -0,0 +1,58 @@ +using System; + +namespace ColorPicker.Models.ColorSpaces; + +public static class OkHsvHelper +{ + /// + /// Converts RGB to OKHSV, returns -1 for undefined channels + /// + /// Red channel + /// Green channel + /// Blue channel + /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Value (0-1) + public static Tuple RgbToOkHsv(double r, double g, double b) + { + var tuple = OkHelper.SrgbToOkHsl(r, g, b); + return new Tuple(tuple.Item1 * 360, tuple.Item2, tuple.Item3); + } + + /// + /// Converts HSL to OKHSV + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) + public static Tuple HslToOkHsv(double h, double s, double l) + { + var rgb = RgbHelper.HslToRgb(h, s, l); + return OkHsvHelper.RgbToOkHsv(rgb.Item1, rgb.Item2, rgb.Item3); + } + + /// + /// Converts HSV to OKHSV + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) + public static Tuple HsvToOkHsv(double h, double s, double v) + { + var rgb = RgbHelper.HsvToRgb(h, s, v); + return OkHsvHelper.RgbToOkHsv(rgb.Item1, rgb.Item2, rgb.Item3); + } + + /// + /// Converts OKHSL to OKHSV + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) + public static Tuple OkHslToOkHsv(double h, double s, double l) + { + var rgb = RgbHelper.OkHslToRgb(h, s, l); + return OkHsvHelper.RgbToOkHsv(rgb.Item1, rgb.Item2, rgb.Item3); + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs b/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs new file mode 100644 index 0000000..a181f9b --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs @@ -0,0 +1,103 @@ +using System; + +namespace ColorPicker.Models.ColorSpaces; + +public static class RgbHelper +{ + /// + /// Converts HSV to RGB + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values (0-1) in order: R, G, B + public static Tuple HsvToRgb(double h, double s, double v) + { + if (s == 0) + // achromatic (grey) + return new Tuple(v, v, v); + if (h >= 360.0) + h = 0; + h /= 60; + var i = (int)h; + var f = h - i; + var p = v * (1 - s); + var q = v * (1 - s * f); + var t = v * (1 - s * (1 - f)); + + switch (i) + { + case 0: return new Tuple(v, t, p); + case 1: return new Tuple(q, v, p); + case 2: return new Tuple(p, v, t); + case 3: return new Tuple(p, q, v); + case 4: return new Tuple(t, p, v); + default: return new Tuple(v, p, q); + } + } + + /// + /// Converts HSL to RGB + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values (0-1) in order: R, G, B + public static Tuple HslToRgb(double h, double s, double l) + { + var hueCircleSegment = (int)(h / 60); + var circleSegmentFraction = (h - 60 * hueCircleSegment) / 60; + + var maxRGB = l < 0.5 ? l * (1 + s) : l + s - l * s; + var minRGB = 2 * l - maxRGB; + var delta = maxRGB - minRGB; + + switch (hueCircleSegment) + { + case 0: + return new Tuple(maxRGB, delta * circleSegmentFraction + minRGB, + minRGB); //red-yellow + case 1: + return new Tuple(delta * (1 - circleSegmentFraction) + minRGB, maxRGB, + minRGB); //yellow-green + case 2: + return new Tuple(minRGB, maxRGB, + delta * circleSegmentFraction + minRGB); //green-cyan + case 3: + return new Tuple(minRGB, delta * (1 - circleSegmentFraction) + minRGB, + maxRGB); //cyan-blue + case 4: + return new Tuple(delta * circleSegmentFraction + minRGB, minRGB, + maxRGB); //blue-purple + default: + return new Tuple(maxRGB, minRGB, + delta * (1 - circleSegmentFraction) + minRGB); //purple-red and invalid values + } + } + + /// + /// Converts OKHSV to RGB + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Value, 0-1 + /// Values (0-1) in order: R, G, B + public static Tuple OkHsvToRgb(double h, double s, double v) + { + var tuple = OkHelper.OkHsvToSrgb(h / 360, s, v); + return new Tuple(tuple.Item1, tuple.Item2, tuple.Item3); + } + + /// + /// Converts OKHSL to RGB + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values (0-1) in order: R, G, B + public static Tuple OkHslToRgb(double h, double s, double l) + { + var tuple = OkHelper.OkHslToSrgb(h / 360.0, s, l); + return new Tuple(tuple.Item1, tuple.Item2, tuple.Item3); + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorState.cs b/src/ColorPicker.Models/ColorState.cs index 77ed4fe..cfbefef 100644 --- a/src/ColorPicker.Models/ColorState.cs +++ b/src/ColorPicker.Models/ColorState.cs @@ -1,4 +1,6 @@ -namespace ColorPicker.Models +using ColorPicker.Models.ColorSpaces; + +namespace ColorPicker.Models { public struct ColorState { @@ -13,9 +15,21 @@ public struct ColorState private double _HSL_H; private double _HSL_S; private double _HSL_L; + + private double _OKHSV_H; + private double _OKHSV_S; + private double _OKHSV_V; + + private double _OKHSL_H; + private double _OKHSL_S; + private double _OKHSL_L; - public ColorState(double rGB_R, double rGB_G, double rGB_B, double a, double hSV_H, double hSV_S, double hSV_V, - double hSL_h, double hSL_s, double hSL_l) + public ColorState( + double rGB_R, double rGB_G, double rGB_B, double a, + double hSV_H, double hSV_S, double hSV_V, + double hSL_h, double hSL_s, double hSL_l, + double oKHSV_H, double oKHSV_S, double oKHSV_V, + double oKHSL_H, double oKHSL_S, double oKHSL_L) { _RGB_R = rGB_R; _RGB_G = rGB_G; @@ -27,6 +41,13 @@ public ColorState(double rGB_R, double rGB_G, double rGB_B, double a, double hSV _HSL_H = hSL_h; _HSL_S = hSL_s; _HSL_L = hSL_l; + + _OKHSV_H = oKHSV_H; + _OKHSV_S = oKHSV_S; + _OKHSV_V = oKHSV_V; + _OKHSL_H = oKHSL_H; + _OKHSL_S = oKHSL_S; + _OKHSL_L = oKHSL_L; } public void SetARGB(double a, double r, double g, double b) @@ -37,6 +58,8 @@ public void SetARGB(double a, double r, double g, double b) _RGB_B = b; RecalculateHSVFromRGB(); RecalculateHSLFromRGB(); + RecalculateOKHSLFromRGB(); + RecalculateOKHSVFromRGB(); } public double A { get; set; } @@ -49,6 +72,8 @@ public double RGB_R _RGB_R = value; RecalculateHSVFromRGB(); RecalculateHSLFromRGB(); + RecalculateOKHSLFromRGB(); + RecalculateOKHSVFromRGB(); } } @@ -60,6 +85,8 @@ public double RGB_G _RGB_G = value; RecalculateHSVFromRGB(); RecalculateHSLFromRGB(); + RecalculateOKHSLFromRGB(); + RecalculateOKHSVFromRGB(); } } @@ -71,6 +98,8 @@ public double RGB_B _RGB_B = value; RecalculateHSVFromRGB(); RecalculateHSLFromRGB(); + RecalculateOKHSLFromRGB(); + RecalculateOKHSVFromRGB(); } } @@ -82,6 +111,8 @@ public double HSV_H _HSV_H = value; RecalculateRGBFromHSV(); RecalculateHSLFromHSV(); + RecalculateOKHSLFromHSV(); + RecalculateOKHSVFromHSV(); } } @@ -93,6 +124,8 @@ public double HSV_S _HSV_S = value; RecalculateRGBFromHSV(); RecalculateHSLFromHSV(); + RecalculateOKHSLFromHSV(); + RecalculateOKHSVFromHSV(); } } @@ -104,6 +137,8 @@ public double HSV_V _HSV_V = value; RecalculateRGBFromHSV(); RecalculateHSLFromHSV(); + RecalculateOKHSLFromHSV(); + RecalculateOKHSVFromHSV(); } } @@ -115,6 +150,8 @@ public double HSL_H _HSL_H = value; RecalculateRGBFromHSL(); RecalculateHSVFromHSL(); + RecalculateOKHSLFromHSL(); + RecalculateOKHSVFromHSL(); } } @@ -126,6 +163,8 @@ public double HSL_S _HSL_S = value; RecalculateRGBFromHSL(); RecalculateHSVFromHSL(); + RecalculateOKHSLFromHSL(); + RecalculateOKHSVFromHSL(); } } @@ -137,12 +176,95 @@ public double HSL_L _HSL_L = value; RecalculateRGBFromHSL(); RecalculateHSVFromHSL(); + RecalculateOKHSLFromHSL(); + RecalculateOKHSVFromHSL(); + } + } + + public double OKHSV_H + { + get => _OKHSV_H; + set + { + _OKHSV_H = value; + RecalculateRGBFromOKHSV(); + RecalculateHSLFromOKHSV(); + RecalculateHSVFromOKHSV(); + RecalculateOKHSLFromOKHSV(); + } + } + + public double OKHSV_S + { + get => _OKHSV_S; + set + { + _OKHSV_S = value; + RecalculateRGBFromOKHSV(); + RecalculateHSLFromOKHSV(); + RecalculateHSVFromOKHSV(); + RecalculateOKHSLFromOKHSV(); } } + public double OKHSV_V + { + get => _OKHSV_V; + set + { + _OKHSV_V = value; + RecalculateRGBFromOKHSV(); + RecalculateHSLFromOKHSV(); + RecalculateHSVFromOKHSV(); + RecalculateOKHSLFromOKHSV(); + } + } + + public double OKHSL_H + { + get => _OKHSL_H; + set + { + _OKHSL_H = value; + RecalculateRGBFromOKHSL(); + RecalculateHSLFromOKHSL(); + RecalculateHSVFromOKHSL(); + RecalculateOKHSVFromOKHSL(); + } + } + + public double OKHSL_S + { + get => _OKHSL_S; + set + { + _OKHSL_S = value; + RecalculateRGBFromOKHSL(); + RecalculateHSLFromOKHSL(); + RecalculateHSVFromOKHSL(); + RecalculateOKHSVFromOKHSL(); + } + } + + public double OKHSL_L + { + get => _OKHSL_L; + set + { + _OKHSL_L = value; + RecalculateRGBFromOKHSL(); + RecalculateHSLFromOKHSL(); + RecalculateHSVFromOKHSL(); + RecalculateOKHSVFromOKHSL(); + } + } + + + + private void RecalculateHSLFromRGB() { - var hsltuple = ColorSpaceHelper.RgbToHsl(_RGB_R, _RGB_G, _RGB_B); + var hsltuple = HslHelper.RgbToHsl(_RGB_R, _RGB_G, _RGB_B); double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; if (h != -1) _HSL_H = h; @@ -153,17 +275,38 @@ private void RecalculateHSLFromRGB() private void RecalculateHSLFromHSV() { - var hsltuple = ColorSpaceHelper.HsvToHsl(_HSV_H, _HSV_S, _HSV_V); + var hsltuple = HslHelper.HsvToHsl(_HSV_H, _HSV_S, _HSV_V); double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; _HSL_H = h; if (s != -1) _HSL_S = s; _HSL_L = l; } + + private void RecalculateHSLFromOKHSV() + { + var hsltuple = HslHelper.OkHsvToHsl(_OKHSV_H, _OKHSV_S, _OKHSV_V); + double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; + _HSL_H = h; + _HSL_S = s;// todo add -1 checks if necessary + _HSL_L = l; + } + + private void RecalculateHSLFromOKHSL() + { + var hsltuple = HslHelper.OkHslToHsl(_OKHSL_H, _OKHSL_S, _OKHSL_L); + double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; + _HSL_H = h; + _HSL_S = s;// todo add -1 checks if necessary + _HSL_L = l; + } + + + private void RecalculateHSVFromRGB() { - var hsvtuple = ColorSpaceHelper.RgbToHsv(_RGB_R, _RGB_G, _RGB_B); + var hsvtuple = HsvHelper.RgbToHsv(_RGB_R, _RGB_G, _RGB_B); double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; if (h != -1) _HSV_H = h; @@ -174,17 +317,38 @@ private void RecalculateHSVFromRGB() private void RecalculateHSVFromHSL() { - var hsvtuple = ColorSpaceHelper.HslToHsv(_HSL_H, _HSL_S, _HSL_L); + var hsvtuple = HsvHelper.HslToHsv(_HSL_H, _HSL_S, _HSL_L); double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; _HSV_H = h; if (s != -1) _HSV_S = s; _HSV_V = v; } + + private void RecalculateHSVFromOKHSV() + { + var hsvtuple = HsvHelper.OkHsvToHsv(_OKHSV_H, _OKHSV_S, _OKHSV_V); + double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; + _HSV_H = h; + _HSV_S = s;// todo add -1 checks if necessary + _HSV_V = v; + } + + private void RecalculateHSVFromOKHSL() + { + var hsvtuple = HsvHelper.OkHslToHsv(_OKHSL_H, _OKHSL_S, _OKHSL_L); + double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; + _HSV_H = h; + _HSV_S = s;// todo add -1 checks if necessary + _HSV_V = v; + } + + + private void RecalculateRGBFromHSL() { - var rgbtuple = ColorSpaceHelper.HslToRgb(_HSL_H, _HSL_S, _HSL_L); + var rgbtuple = RgbHelper.HslToRgb(_HSL_H, _HSL_S, _HSL_L); _RGB_R = rgbtuple.Item1; _RGB_G = rgbtuple.Item2; _RGB_B = rgbtuple.Item3; @@ -192,10 +356,96 @@ private void RecalculateRGBFromHSL() private void RecalculateRGBFromHSV() { - var rgbtuple = ColorSpaceHelper.HsvToRgb(_HSV_H, _HSV_S, _HSV_V); + var rgbtuple = RgbHelper.HsvToRgb(_HSV_H, _HSV_S, _HSV_V); _RGB_R = rgbtuple.Item1; _RGB_G = rgbtuple.Item2; _RGB_B = rgbtuple.Item3; } + + private void RecalculateRGBFromOKHSL() + { + var rgbtuple = RgbHelper.OkHslToRgb(_OKHSL_H, _OKHSL_S, _OKHSL_L); + _RGB_R = rgbtuple.Item1; + _RGB_G = rgbtuple.Item2;// todo add -1 checks if necessary + _RGB_B = rgbtuple.Item3; + } + + private void RecalculateRGBFromOKHSV() + { + var rgbtuple = RgbHelper.OkHsvToRgb(_OKHSV_H, _OKHSV_S, _OKHSV_V); + _RGB_R = rgbtuple.Item1; + _RGB_G = rgbtuple.Item2;// todo add -1 checks if necessary + _RGB_B = rgbtuple.Item3; + } + + + + + private void RecalculateOKHSLFromRGB() + { + var okhsltuple = OkHslHelper.RgbToOkHsl(_RGB_R, _RGB_G, _RGB_B); + _OKHSL_H = okhsltuple.Item1; + _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary + _OKHSL_L = okhsltuple.Item3; + } + + private void RecalculateOKHSLFromHSL() + { + var okhsltuple = OkHslHelper.HslToOkHsl(_HSL_H, _HSL_S, _HSL_L); + _OKHSL_H = okhsltuple.Item1; + _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary + _OKHSL_L = okhsltuple.Item3; + } + + private void RecalculateOKHSLFromHSV() + { + var okhsltuple = OkHslHelper.HsvToOkHsl(_HSV_H, _HSV_S, _HSV_V); + _OKHSL_H = okhsltuple.Item1; + _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary + _OKHSL_L = okhsltuple.Item3; + } + + private void RecalculateOKHSLFromOKHSV() + { + var okhsltuple = OkHslHelper.OkHsvToOkHsl(_OKHSV_H, _OKHSV_S, _OKHSV_V); + _OKHSL_H = okhsltuple.Item1; + _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary + _OKHSL_L = okhsltuple.Item3; + } + + + + + private void RecalculateOKHSVFromRGB() + { + var okhsvtuple = OkHsvHelper.RgbToOkHsv(_RGB_R, _RGB_G, _RGB_B); + _OKHSV_H = okhsvtuple.Item1; + _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary + _OKHSV_V = okhsvtuple.Item3; + } + + private void RecalculateOKHSVFromHSL() + { + var okhsvtuple = OkHsvHelper.HslToOkHsv(_HSL_H, _HSL_S, _HSL_L); + _OKHSV_H = okhsvtuple.Item1; + _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary + _OKHSV_V = okhsvtuple.Item3; + } + + private void RecalculateOKHSVFromHSV() + { + var okhsvtuple = OkHsvHelper.HsvToOkHsv(_HSV_H, _HSV_S, _HSV_V); + _OKHSV_H = okhsvtuple.Item1; + _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary + _OKHSV_V = okhsvtuple.Item3; + } + + private void RecalculateOKHSVFromOKHSL() + { + var okhsvtuple = OkHsvHelper.OkHslToOkHsv(_OKHSL_H, _OKHSL_S, _OKHSL_L); + _OKHSV_H = okhsvtuple.Item1; + _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary + _OKHSV_V = okhsvtuple.Item3; + } } } \ No newline at end of file diff --git a/src/ColorPicker.Models/NotifyableColor.cs b/src/ColorPicker.Models/NotifyableColor.cs index 6b1a9ec..30a7494 100644 --- a/src/ColorPicker.Models/NotifyableColor.cs +++ b/src/ColorPicker.Models/NotifyableColor.cs @@ -139,6 +139,84 @@ public double HSL_L } } + public double OKHSV_H + { + get => storage.ColorState.OKHSV_H; + set + { + if(isUpdating) return; + + var state = storage.ColorState; + state.OKHSV_H = value; + storage.ColorState = state; + } + } + + public double OKHSV_S + { + get => storage.ColorState.OKHSV_S * 100; + set + { + if(isUpdating) return; + + var state = storage.ColorState; + state.OKHSV_S = value / 100; + storage.ColorState = state; + } + } + + public double OKHSV_V + { + get => storage.ColorState.OKHSV_V * 100; + set + { + if(isUpdating) return; + + var state = storage.ColorState; + state.OKHSV_V = value / 100; + storage.ColorState = state; + } + } + + public double OKHSL_H + { + get => storage.ColorState.OKHSL_H; + set + { + if(isUpdating) return; + + var state = storage.ColorState; + state.OKHSL_H = value; + storage.ColorState = state; + } + } + + public double OKHSL_S + { + get => storage.ColorState.OKHSL_S * 100; + set + { + if(isUpdating) return; + + var state = storage.ColorState; + state.OKHSL_S = value / 100; + storage.ColorState = state; + } + } + + public double OKHSL_L + { + get => storage.ColorState.OKHSL_L * 100; + set + { + if(isUpdating) return; + + var state = storage.ColorState; + state.OKHSL_L = value / 100; + storage.ColorState = state; + } + } + public void UpdateEverything(ColorState oldValue) { var currentValue = storage.ColorState; @@ -157,6 +235,14 @@ public void UpdateEverything(ColorState oldValue) if (currentValue.HSL_H != oldValue.HSL_H) RaisePropertyChanged(nameof(HSL_H)); if (currentValue.HSL_S != oldValue.HSL_S) RaisePropertyChanged(nameof(HSL_S)); if (currentValue.HSL_L != oldValue.HSL_L) RaisePropertyChanged(nameof(HSL_L)); + + if (currentValue.OKHSV_H != oldValue.OKHSV_H) RaisePropertyChanged(nameof(OKHSV_H)); + if (currentValue.OKHSV_S != oldValue.OKHSV_S) RaisePropertyChanged(nameof(OKHSV_S)); + if (currentValue.OKHSV_V != oldValue.OKHSV_V) RaisePropertyChanged(nameof(OKHSV_V)); + + if (currentValue.OKHSL_H != oldValue.OKHSL_H) RaisePropertyChanged(nameof(OKHSL_H)); + if (currentValue.OKHSL_S != oldValue.OKHSL_S) RaisePropertyChanged(nameof(OKHSL_S)); + if (currentValue.OKHSL_L != oldValue.OKHSL_L) RaisePropertyChanged(nameof(OKHSL_L)); isUpdating = false; } } diff --git a/src/ColorPicker.Models/PickerType.cs b/src/ColorPicker.Models/PickerType.cs index 1ca04ad..e1bb1b8 100644 --- a/src/ColorPicker.Models/PickerType.cs +++ b/src/ColorPicker.Models/PickerType.cs @@ -3,6 +3,8 @@ public enum PickerType { HSV = 0, - HSL = 1 + HSL = 1, + OKHSV = 2, + OKHSL = 3 } } \ No newline at end of file diff --git a/src/ColorPicker/AlphaSlider.xaml b/src/ColorPicker/AlphaSlider.xaml index 968d512..cdebece 100644 --- a/src/ColorPicker/AlphaSlider.xaml +++ b/src/ColorPicker/AlphaSlider.xaml @@ -4,7 +4,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:ColorPicker" - xmlns:ui="clr-namespace:ColorPicker.UIExtensions" + xmlns:ui="clr-namespace:ColorPicker.ColorSlider" + xmlns:mcs="clr-namespace:ColorPicker.Models.ColorSliders;assembly=ColorPicker.Models" mc:Ignorable="d" x:Name="uc" d:DesignHeight="450" d:DesignWidth="800"> @@ -17,7 +18,7 @@ - diff --git a/src/ColorPicker/UIExtensions/PreviewColorSlider.cs b/src/ColorPicker/ColorSlider/PreviewColorSlider.cs similarity index 51% rename from src/ColorPicker/UIExtensions/PreviewColorSlider.cs rename to src/ColorPicker/ColorSlider/PreviewColorSlider.cs index 92a88da..cbd4296 100644 --- a/src/ColorPicker/UIExtensions/PreviewColorSlider.cs +++ b/src/ColorPicker/ColorSlider/PreviewColorSlider.cs @@ -1,13 +1,15 @@ -using System.ComponentModel; +using System.Collections.Generic; +using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using ColorPicker.Models; +using ColorPicker.Models.ColorSliders; -namespace ColorPicker.UIExtensions +namespace ColorPicker.ColorSlider { - internal abstract class PreviewColorSlider : Slider, INotifyPropertyChanged + internal sealed class PreviewColorSlider : Slider, INotifyPropertyChanged { public static readonly DependencyProperty CurrentColorStateProperty = DependencyProperty.Register(nameof(CurrentColorState), typeof(ColorState), typeof(PreviewColorSlider), @@ -16,25 +18,17 @@ internal abstract class PreviewColorSlider : Slider, INotifyPropertyChanged public static readonly DependencyProperty SmallChangeBindableProperty = DependencyProperty.Register(nameof(SmallChangeBindable), typeof(double), typeof(PreviewColorSlider), new PropertyMetadata(1.0, SmallChangeBindableChangedCallback)); - - private readonly LinearGradientBrush backgroundBrush = new LinearGradientBrush(); - - private SolidColorBrush _leftCapColor = new SolidColorBrush(); - - private SolidColorBrush _rightCapColor = new SolidColorBrush(); - - public PreviewColorSlider() + + public static readonly DependencyProperty SliderTypeProperty = + DependencyProperty.Register(nameof(SliderTypeProperty), typeof(ColorSliderType), typeof(PreviewColorSlider), + new PropertyMetadata(ColorSliderType.RgbRed, ColorSliderTypeChangedCallback)); + + public ColorSliderType SliderType { - Minimum = 0; - Maximum = 255; - SmallChange = 1; - LargeChange = 10; - MinHeight = 12; - PreviewMouseWheel += OnPreviewMouseWheel; + get => (ColorSliderType)GetValue(SliderTypeProperty); + set => SetValue(SliderTypeProperty, value); } - - protected virtual bool RefreshGradient => true; - + public double SmallChangeBindable { get => (double)GetValue(SmallChangeBindableProperty); @@ -46,35 +40,52 @@ public ColorState CurrentColorState get => (ColorState)GetValue(CurrentColorStateProperty); set => SetValue(CurrentColorStateProperty, value); } - + + public GradientStopCollection BackgroundGradient { get => backgroundBrush.GradientStops; set => backgroundBrush.GradientStops = value; } + + private readonly LinearGradientBrush backgroundBrush = new(); + private SolidColorBrush leftCapColor = new(); + private SolidColorBrush rightCapColor = new(); + private IColorSliderType colorSliderTypeImpl; + public SolidColorBrush LeftCapColor { - get => _leftCapColor; + get => leftCapColor; set { - _leftCapColor = value; + leftCapColor = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LeftCapColor))); } } public SolidColorBrush RightCapColor { - get => _rightCapColor; + get => rightCapColor; set { - _rightCapColor = value; + rightCapColor = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RightCapColor))); } } public event PropertyChangedEventHandler PropertyChanged; + public PreviewColorSlider() + { + Minimum = 0; + Maximum = 255; + SmallChange = 1; + LargeChange = 10; + MinHeight = 12; + PreviewMouseWheel += OnPreviewMouseWheel; + } + public override void EndInit() { base.EndInit(); @@ -82,12 +93,32 @@ public override void EndInit() GenerateBackground(); } - protected abstract void GenerateBackground(); + private void GenerateBackground() + { + if (colorSliderTypeImpl is null) + return; + + List points = colorSliderTypeImpl.CalculateRgbGradient(CurrentColorState); - protected static void ColorStateChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + + int lastIndex = points.Count - 1; + LeftCapColor.Color = Color.FromArgb((byte)(points[0].A * 255), (byte)(points[0].R * 255), (byte)(points[0].G * 255), (byte)(points[0].B * 255)); + RightCapColor.Color = Color.FromArgb((byte)(points[lastIndex].A * 255), (byte)(points[lastIndex].R * 255), (byte)(points[lastIndex].G * 255), (byte)(points[lastIndex].B * 255)); + + GradientStopCollection collection = new(points.Count); + foreach (ColorSliderGradientPoint point in points) + { + GradientStop stop = new(Color.FromArgb((byte)(point.A * 255), (byte)(point.R * 255), (byte)(point.G * 255), (byte)(point.B * 255)), point.Position); + collection.Add(stop); + } + + BackgroundGradient = collection; + } + + private static void ColorStateChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { var slider = (PreviewColorSlider)d; - if (slider.RefreshGradient) + if (slider.colorSliderTypeImpl?.RefreshGradient ?? false) slider.GenerateBackground(); } @@ -95,6 +126,13 @@ private static void SmallChangeBindableChangedCallback(DependencyObject d, Depen { ((PreviewColorSlider)d).SmallChange = (double)e.NewValue; } + + private static void ColorSliderTypeChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = (PreviewColorSlider)d; + self.colorSliderTypeImpl = ColorSliderTypeFactory.Get((ColorSliderType)e.NewValue); + self.GenerateBackground(); + } private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs args) { diff --git a/src/ColorPicker/ColorSliders.xaml b/src/ColorPicker/ColorSliders.xaml index bf4450f..ab8d8ea 100644 --- a/src/ColorPicker/ColorSliders.xaml +++ b/src/ColorPicker/ColorSliders.xaml @@ -6,7 +6,8 @@ xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors" xmlns:local="clr-namespace:ColorPicker" xmlns:conv="clr-namespace:ColorPicker.Converters" - xmlns:ui="clr-namespace:ColorPicker.UIExtensions" + xmlns:ui="clr-namespace:ColorPicker.ColorSlider" + xmlns:mcs="clr-namespace:ColorPicker.Models.ColorSliders;assembly=ColorPicker.Models" xmlns:behaviors="clr-namespace:ColorPicker.Behaviors" mc:Ignorable="d" MinWidth="200" @@ -56,11 +57,11 @@ - + @@ -75,11 +76,11 @@ - + @@ -94,11 +95,11 @@ - + @@ -114,12 +115,12 @@ - + @@ -137,7 +138,7 @@ - + @@ -163,15 +164,15 @@ - + - @@ -182,15 +183,15 @@ - + - @@ -200,16 +201,16 @@ - - + + - @@ -221,11 +222,12 @@ - + @@ -243,7 +245,7 @@ - + @@ -268,40 +270,58 @@ - - - + + + + + + + + + - - - + + + + + + + + + - - - + + + + + + + + + @@ -309,12 +329,12 @@ - + @@ -323,8 +343,8 @@ - diff --git a/src/ColorPicker/DualPickerControlBase.cs b/src/ColorPicker/DualPickerControlBase.cs index d7d9930..db6b7d3 100644 --- a/src/ColorPicker/DualPickerControlBase.cs +++ b/src/ColorPicker/DualPickerControlBase.cs @@ -9,7 +9,7 @@ public class DualPickerControlBase : PickerControlBase, ISecondColorStorage, IHi { public static readonly DependencyProperty SecondColorStateProperty = DependencyProperty.Register(nameof(SecondColorState), typeof(ColorState), typeof(DualPickerControlBase), - new PropertyMetadata(new ColorState(1, 1, 1, 1, 0, 0, 1, 0, 0, 1), OnSecondColorStatePropertyChange)); + new PropertyMetadata(new ColorState(1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1), OnSecondColorStatePropertyChange)); public static readonly DependencyProperty SecondaryColorProperty = DependencyProperty.Register(nameof(SecondaryColor), typeof(Color), typeof(DualPickerControlBase), @@ -18,7 +18,7 @@ public class DualPickerControlBase : PickerControlBase, ISecondColorStorage, IHi public static readonly DependencyProperty HintColorStateProperty = DependencyProperty.Register(nameof(HintColorState), typeof(ColorState), typeof(DualPickerControlBase), - new PropertyMetadata(new ColorState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), OnHintColorStatePropertyChange)); + new PropertyMetadata(new ColorState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), OnHintColorStatePropertyChange)); public static readonly DependencyProperty HintColorProperty = DependencyProperty.Register(nameof(HintColor), typeof(Color), typeof(DualPickerControlBase), diff --git a/src/ColorPicker/Images/CircularHueGradientOkhsl.png b/src/ColorPicker/Images/CircularHueGradientOkhsl.png new file mode 100644 index 0000000000000000000000000000000000000000..d477ad57f0c5c6d83d350a3dc4e9956baf293f70 GIT binary patch literal 33736 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 + SmallChange="{Binding ElementName=uc, Path=SmallChange}"> + + + + diff --git a/src/ColorPicker/StandardColorPicker.xaml b/src/ColorPicker/StandardColorPicker.xaml index af5073e..0d837a9 100644 --- a/src/ColorPicker/StandardColorPicker.xaml +++ b/src/ColorPicker/StandardColorPicker.xaml @@ -33,6 +33,8 @@ SelectedIndex="{Binding ElementName=uc, Path=PickerType, Converter={StaticResource PickerTypeToIntConverter}}"> HSV HSL + OKHSV + OKHSL + xmlns:colorSlider="clr-namespace:ColorPicker.ColorSlider"> - - \ No newline at end of file diff --git a/src/ColorPicker/UIExtensions/HslColorSlider.cs b/src/ColorPicker/UIExtensions/HslColorSlider.cs deleted file mode 100644 index 4769a66..0000000 --- a/src/ColorPicker/UIExtensions/HslColorSlider.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Windows; -using System.Windows.Media; -using ColorPicker.Models; - -namespace ColorPicker.UIExtensions -{ - internal class HslColorSlider : PreviewColorSlider - { - public static readonly DependencyProperty SliderHslTypeProperty = - DependencyProperty.Register(nameof(SliderHslType), typeof(string), typeof(HslColorSlider), - new PropertyMetadata("")); - - protected override bool RefreshGradient => SliderHslType != "H"; - - public string SliderHslType - { - get => (string)GetValue(SliderHslTypeProperty); - set => SetValue(SliderHslTypeProperty, value); - } - - protected override void GenerateBackground() - { - if (SliderHslType == "H") - { - var colorStart = GetColorForSelectedArgb(0); - var colorEnd = GetColorForSelectedArgb(360); - LeftCapColor.Color = colorStart; - RightCapColor.Color = colorEnd; - BackgroundGradient = new GradientStopCollection - { - new GradientStop(colorStart, 0), - new GradientStop(GetColorForSelectedArgb(60), 1 / 6.0), - new GradientStop(GetColorForSelectedArgb(120), 2 / 6.0), - new GradientStop(GetColorForSelectedArgb(180), 0.5), - new GradientStop(GetColorForSelectedArgb(240), 4 / 6.0), - new GradientStop(GetColorForSelectedArgb(300), 5 / 6.0), - new GradientStop(colorEnd, 1) - }; - return; - } - - if (SliderHslType == "L") - { - var colorStart = GetColorForSelectedArgb(0); - var colorEnd = GetColorForSelectedArgb(255); - LeftCapColor.Color = colorStart; - RightCapColor.Color = colorEnd; - BackgroundGradient = new GradientStopCollection - { - new GradientStop(colorStart, 0), - new GradientStop(GetColorForSelectedArgb(128), 0.5), - new GradientStop(colorEnd, 1) - }; - return; - } - - { - var colorStart = GetColorForSelectedArgb(0); - var colorEnd = GetColorForSelectedArgb(255); - LeftCapColor.Color = colorStart; - RightCapColor.Color = colorEnd; - BackgroundGradient = new GradientStopCollection - { - new GradientStop(colorStart, 0.0), - new GradientStop(colorEnd, 1) - }; - } - } - - private Color GetColorForSelectedArgb(int value) - { - switch (SliderHslType) - { - case "H": - { - var rgbtuple = ColorSpaceHelper.HslToRgb(value, 1.0, 0.5); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); - } - case "S": - { - var rgbtuple = ColorSpaceHelper.HslToRgb(CurrentColorState.HSL_H, value / 255.0, - CurrentColorState.HSL_L); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); - } - case "L": - { - var rgbtuple = ColorSpaceHelper.HslToRgb(CurrentColorState.HSL_H, CurrentColorState.HSL_S, - value / 255.0); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); - } - default: - return Color.FromArgb(255, (byte)(CurrentColorState.RGB_R * 255), - (byte)(CurrentColorState.RGB_G * 255), (byte)(CurrentColorState.RGB_B * 255)); - } - } - } -} \ No newline at end of file diff --git a/src/ColorPicker/UIExtensions/HsvColorSlider.cs b/src/ColorPicker/UIExtensions/HsvColorSlider.cs deleted file mode 100644 index fbe5a7b..0000000 --- a/src/ColorPicker/UIExtensions/HsvColorSlider.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Windows; -using System.Windows.Media; -using ColorPicker.Models; - -namespace ColorPicker.UIExtensions -{ - internal class HsvColorSlider : PreviewColorSlider - { - public static readonly DependencyProperty SliderHsvTypeProperty = - DependencyProperty.Register(nameof(SliderHsvType), typeof(string), typeof(HsvColorSlider), - new PropertyMetadata("")); - - protected override bool RefreshGradient => SliderHsvType != "H"; - - public string SliderHsvType - { - get => (string)GetValue(SliderHsvTypeProperty); - set => SetValue(SliderHsvTypeProperty, value); - } - - protected override void GenerateBackground() - { - if (SliderHsvType == "H") - { - var colorStart = GetColorForSelectedArgb(0); - var colorEnd = GetColorForSelectedArgb(360); - LeftCapColor.Color = colorStart; - RightCapColor.Color = colorEnd; - BackgroundGradient = new GradientStopCollection - { - new GradientStop(colorStart, 0), - new GradientStop(GetColorForSelectedArgb(60), 1 / 6.0), - new GradientStop(GetColorForSelectedArgb(120), 2 / 6.0), - new GradientStop(GetColorForSelectedArgb(180), 0.5), - new GradientStop(GetColorForSelectedArgb(240), 4 / 6.0), - new GradientStop(GetColorForSelectedArgb(300), 5 / 6.0), - new GradientStop(colorEnd, 1) - }; - return; - } - - { - var colorStart = GetColorForSelectedArgb(0); - var colorEnd = GetColorForSelectedArgb(255); - LeftCapColor.Color = colorStart; - RightCapColor.Color = colorEnd; - BackgroundGradient = new GradientStopCollection - { - new GradientStop(colorStart, 0.0), - new GradientStop(colorEnd, 1) - }; - } - } - - private Color GetColorForSelectedArgb(int value) - { - switch (SliderHsvType) - { - case "H": - { - var rgbtuple = ColorSpaceHelper.HsvToRgb(value, 1.0, 1.0); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); - } - case "S": - { - var rgbtuple = ColorSpaceHelper.HsvToRgb(CurrentColorState.HSV_H, value / 255.0, - CurrentColorState.HSV_V); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); - } - case "V": - { - var rgbtuple = ColorSpaceHelper.HsvToRgb(CurrentColorState.HSV_H, CurrentColorState.HSV_S, - value / 255.0); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); - } - default: - return Color.FromArgb((byte)(CurrentColorState.A * 255), (byte)(CurrentColorState.RGB_R * 255), - (byte)(CurrentColorState.RGB_G * 255), (byte)(CurrentColorState.RGB_B * 255)); - } - } - } -} \ No newline at end of file diff --git a/src/ColorPicker/UIExtensions/RgbColorSlider.cs b/src/ColorPicker/UIExtensions/RgbColorSlider.cs deleted file mode 100644 index b1ef942..0000000 --- a/src/ColorPicker/UIExtensions/RgbColorSlider.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Windows; -using System.Windows.Media; - -namespace ColorPicker.UIExtensions -{ - internal class RgbColorSlider : PreviewColorSlider - { - public static readonly DependencyProperty SliderArgbTypeProperty = - DependencyProperty.Register(nameof(SliderArgbType), typeof(string), typeof(RgbColorSlider), - new PropertyMetadata("")); - - public string SliderArgbType - { - get => (string)GetValue(SliderArgbTypeProperty); - set => SetValue(SliderArgbTypeProperty, value); - } - - protected override void GenerateBackground() - { - var colorStart = GetColorForSelectedArgb(0); - var colorEnd = GetColorForSelectedArgb(255); - LeftCapColor.Color = colorStart; - RightCapColor.Color = colorEnd; - BackgroundGradient = new GradientStopCollection - { - new GradientStop(colorStart, 0.0), - new GradientStop(colorEnd, 1) - }; - } - - private Color GetColorForSelectedArgb(int value) - { - var a = (byte)(CurrentColorState.A * 255); - var r = (byte)(CurrentColorState.RGB_R * 255); - var g = (byte)(CurrentColorState.RGB_G * 255); - var b = (byte)(CurrentColorState.RGB_B * 255); - switch (SliderArgbType) - { - case "A": return Color.FromArgb((byte)value, r, g, b); - case "R": return Color.FromArgb(255, (byte)value, g, b); - case "G": return Color.FromArgb(255, r, (byte)value, b); - case "B": return Color.FromArgb(255, r, g, (byte)value); - default: return Color.FromArgb(a, r, g, b); - } - - ; - } - } -} \ No newline at end of file diff --git a/src/ColorPicker/UserControls/SquareSlider.xaml.cs b/src/ColorPicker/UserControls/SquareSlider.xaml.cs index 88bcd3e..c4093b3 100644 --- a/src/ColorPicker/UserControls/SquareSlider.xaml.cs +++ b/src/ColorPicker/UserControls/SquareSlider.xaml.cs @@ -1,11 +1,13 @@ using System; using System.ComponentModel; +using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using ColorPicker.Models; +using ColorPicker.Models.ColorSpaces; namespace ColorPicker.UserControls { @@ -32,8 +34,7 @@ public static readonly DependencyProperty PickerTypeProperty private double _rangeX; private double _rangeY; - private Func> colorSpaceConversionMethod = - ColorSpaceHelper.HsvToRgb; + private Func> colorSpaceConversionMethod = RgbHelper.HsvToRgb; public SquareSlider() { @@ -41,7 +42,7 @@ public SquareSlider() InitializeComponent(); RecalculateGradient(); } - + public double Hue { get => (double)GetValue(HueProperty); @@ -121,10 +122,24 @@ private void RecalculateGradient() private static void OnColorSpaceChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) { var sender = (SquareSlider)d; - if ((PickerType)args.NewValue == PickerType.HSV) - sender.colorSpaceConversionMethod = ColorSpaceHelper.HsvToRgb; - else - sender.colorSpaceConversionMethod = ColorSpaceHelper.HslToRgb; + switch ((PickerType)args.NewValue) + { + case PickerType.HSV: + sender.colorSpaceConversionMethod = RgbHelper.HsvToRgb; + break; + case PickerType.HSL: + sender.colorSpaceConversionMethod = RgbHelper.HslToRgb; + break; + case PickerType.OKHSV: + sender.colorSpaceConversionMethod = RgbHelper.OkHsvToRgb; + break; + case PickerType.OKHSL: + sender.colorSpaceConversionMethod = RgbHelper.OkHslToRgb; + break; + default: + sender.colorSpaceConversionMethod = RgbHelper.HslToRgb; + break; + } sender.RecalculateGradient(); } From a7379b4e45ff2ab74f17129290388d1d059330be Mon Sep 17 00:00:00 2001 From: CPKreuz Date: Thu, 23 Nov 2023 15:50:49 +0100 Subject: [PATCH 2/2] Detupled everything --- .../DualPickerControlBase.cs | 4 +- .../PickerControlBase.cs | 2 +- src/ColorPicker.AvaloniaUI/SquareSlider.cs | 53 +- .../UIExtensions/HslColorSlider.cs | 20 +- .../UIExtensions/HsvColorSlider.cs | 20 +- .../ColorPicker.Models.csproj | 1 + .../ColorSliders/ColorSliderGradientPoint.cs | 9 +- .../ColorSpaces/HslHelper.cs | 21 +- .../ColorSpaces/HsvHelper.cs | 19 +- .../ColorSpaces/OkHelper.cs | 933 ++++++++---------- .../ColorSpaces/OkHslHelper.cs | 19 +- .../ColorSpaces/OkHsvHelper.cs | 19 +- .../ColorSpaces/RgbHelper.cs | 43 +- src/ColorPicker.Models/ColorState.cs | 164 ++- src/ColorPicker.Models/Colors/Hsl.cs | 17 + src/ColorPicker.Models/Colors/Hsv.cs | 17 + src/ColorPicker.Models/Colors/Lab.cs | 17 + src/ColorPicker.Models/Colors/Rgb.cs | 17 + .../UserControls/SquareSlider.xaml.cs | 89 +- 19 files changed, 781 insertions(+), 703 deletions(-) create mode 100644 src/ColorPicker.Models/Colors/Hsl.cs create mode 100644 src/ColorPicker.Models/Colors/Hsv.cs create mode 100644 src/ColorPicker.Models/Colors/Lab.cs create mode 100644 src/ColorPicker.Models/Colors/Rgb.cs diff --git a/src/ColorPicker.AvaloniaUI/DualPickerControlBase.cs b/src/ColorPicker.AvaloniaUI/DualPickerControlBase.cs index 8f6dfc8..7cec09d 100644 --- a/src/ColorPicker.AvaloniaUI/DualPickerControlBase.cs +++ b/src/ColorPicker.AvaloniaUI/DualPickerControlBase.cs @@ -9,11 +9,11 @@ public class DualPickerControlBase : PickerControlBase, ISecondColorStorage, IHi { public static readonly StyledProperty SecondColorStateProperty = AvaloniaProperty.Register( - nameof(SecondColorState), new ColorState(1, 1, 1, 1, 0, 0, 1, 0, 0, 1)); + nameof(SecondColorState), new ColorState(1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1)); public static readonly StyledProperty HintColorStateProperty = AvaloniaProperty.Register( - nameof(HintColorState), new ColorState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + nameof(HintColorState), new ColorState(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); public static readonly StyledProperty SecondaryColorProperty = AvaloniaProperty.Register( diff --git a/src/ColorPicker.AvaloniaUI/PickerControlBase.cs b/src/ColorPicker.AvaloniaUI/PickerControlBase.cs index 83075a5..62a0be3 100644 --- a/src/ColorPicker.AvaloniaUI/PickerControlBase.cs +++ b/src/ColorPicker.AvaloniaUI/PickerControlBase.cs @@ -11,7 +11,7 @@ public class PickerControlBase : TemplatedControl, IColorStateStorage { public static readonly StyledProperty ColorStateProperty = AvaloniaProperty.Register( - nameof(ColorState), new ColorState(0, 0, 0, 1, 0, 0, 0, 0, 0, 0)); + nameof(ColorState), new ColorState(0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); public static readonly StyledProperty SelectedColorProperty = AvaloniaProperty.Register( diff --git a/src/ColorPicker.AvaloniaUI/SquareSlider.cs b/src/ColorPicker.AvaloniaUI/SquareSlider.cs index 6ec53ba..343e9e2 100644 --- a/src/ColorPicker.AvaloniaUI/SquareSlider.cs +++ b/src/ColorPicker.AvaloniaUI/SquareSlider.cs @@ -11,6 +11,7 @@ using Avalonia.Reactive; using ColorPicker.AvaloniaUI; using ColorPicker.Models; +using ColorPicker.Models.ColorSpaces; namespace ColorPicker.UserControls; @@ -50,9 +51,8 @@ public NotifyableColor Color set => SetValue(ColorProperty, value); } - private Func> colorSpaceConversionMethod = - ColorSpaceHelper.HsvToRgb; - + private Action recalculateGradientMethod; + private IDisposable headXBinding; private IDisposable headYBinding; private Image image; @@ -111,6 +111,8 @@ public SquareSlider() { GradientBitmap = new WriteableBitmap(new PixelSize(32, 32), new Vector(96, 96), PixelFormats.Rgb24); PseudoClasses.Set(":hsv", true); + + recalculateGradientMethod = RecalculateGradientHsv; } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -119,7 +121,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) image = e.NameScope.Find("PART_GradientImage"); UpdateHeadBindings(this, PickerType); - RecalculateGradient(); + recalculateGradientMethod(); } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -142,7 +144,31 @@ protected override void OnPointerMoved(PointerEventArgs e) UpdatePos(e.GetPosition(this)); } - private void RecalculateGradient() + private void RecalculateGradientHsv() + { + var w = GradientBitmap.PixelSize.Width; + var h = GradientBitmap.PixelSize.Height; + var hue = Hue; + var pixels = new byte[w * h * 3]; + for (var j = 0; j < h; j++) + for (var i = 0; i < w; i++) + { + var rgb = RgbHelper.HsvToRgb(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); + var pos = (j * h + i) * 3; + pixels[pos] = (byte)(rgb.R * 255); + pixels[pos + 1] = (byte)(rgb.G * 255); + pixels[pos + 2] = (byte)(rgb.B * 255); + } + + using (var framebuffer = GradientBitmap.Lock()) + { + framebuffer.WritePixels(0, 0, w, h, pixels); + } + + image.InvalidateVisual(); + } + + private void RecalculateGradientHsl() { var w = GradientBitmap.PixelSize.Width; var h = GradientBitmap.PixelSize.Height; @@ -151,12 +177,11 @@ private void RecalculateGradient() for (var j = 0; j < h; j++) for (var i = 0; i < w; i++) { - var rgbtuple = colorSpaceConversionMethod(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; + var rgb = RgbHelper.HslToRgb(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); var pos = (j * h + i) * 3; - pixels[pos] = (byte)(r * 255); - pixels[pos + 1] = (byte)(g * 255); - pixels[pos + 2] = (byte)(b * 255); + pixels[pos] = (byte)(rgb.R * 255); + pixels[pos + 1] = (byte)(rgb.G * 255); + pixels[pos + 2] = (byte)(rgb.B * 255); } using (var framebuffer = GradientBitmap.Lock()) @@ -171,11 +196,11 @@ private static void OnColorSpaceChanged(AvaloniaPropertyChangedEventArgs args) { - ((SquareSlider)args.Sender).RecalculateGradient(); + ((SquareSlider)args.Sender).recalculateGradientMethod(); } private void UpdatePos(Point pos) diff --git a/src/ColorPicker.AvaloniaUI/UIExtensions/HslColorSlider.cs b/src/ColorPicker.AvaloniaUI/UIExtensions/HslColorSlider.cs index e184753..31be2d2 100644 --- a/src/ColorPicker.AvaloniaUI/UIExtensions/HslColorSlider.cs +++ b/src/ColorPicker.AvaloniaUI/UIExtensions/HslColorSlider.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Media; using ColorPicker.Models; +using ColorPicker.Models.ColorSpaces; namespace ColorPicker.UIExtensions; @@ -73,23 +74,20 @@ private Color GetColorForSelectedArgb(int value) { case "H": { - var rgbtuple = ColorSpaceHelper.HslToRgb(value, 1.0, 0.5); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + var rgb = RgbHelper.HslToRgb(value, 1.0, 0.5); + return Color.FromArgb(255, (byte)(rgb.R * 255), (byte)(rgb.G * 255), (byte)(rgb.B * 255)); } case "S": { - var rgbtuple = - ColorSpaceHelper.HslToRgb(CurrentColorState.HSL_H, value / 255.0, CurrentColorState.HSL_L); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + var rgb = + RgbHelper.HslToRgb(CurrentColorState.HSL_H, value / 255.0, CurrentColorState.HSL_L); + return Color.FromArgb(255, (byte)(rgb.R * 255), (byte)(rgb.G * 255), (byte)(rgb.B * 255)); } case "L": { - var rgbtuple = - ColorSpaceHelper.HslToRgb(CurrentColorState.HSL_H, CurrentColorState.HSL_S, value / 255.0); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + var rgb = + RgbHelper.HslToRgb(CurrentColorState.HSL_H, CurrentColorState.HSL_S, value / 255.0); + return Color.FromArgb(255, (byte)(rgb.R * 255), (byte)(rgb.G * 255), (byte)(rgb.B * 255)); } default: return Color.FromArgb(255, (byte)(CurrentColorState.RGB_R * 255), (byte)(CurrentColorState.RGB_G * 255), diff --git a/src/ColorPicker.AvaloniaUI/UIExtensions/HsvColorSlider.cs b/src/ColorPicker.AvaloniaUI/UIExtensions/HsvColorSlider.cs index 2181feb..a0c7a82 100644 --- a/src/ColorPicker.AvaloniaUI/UIExtensions/HsvColorSlider.cs +++ b/src/ColorPicker.AvaloniaUI/UIExtensions/HsvColorSlider.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Media; using ColorPicker.Models; +using ColorPicker.Models.ColorSpaces; namespace ColorPicker.UIExtensions; @@ -58,23 +59,20 @@ private Color GetColorForSelectedArgb(int value) { case "H": { - var rgbtuple = ColorSpaceHelper.HsvToRgb(value, 1.0, 1.0); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + var rgb = RgbHelper.HsvToRgb(value, 1.0, 1.0); + return Color.FromArgb(255, (byte)(rgb.R * 255), (byte)(rgb.G * 255), (byte)(rgb.B * 255)); } case "S": { - var rgbtuple = - ColorSpaceHelper.HsvToRgb(CurrentColorState.HSV_H, value / 255.0, CurrentColorState.HSV_V); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + var rgb = + RgbHelper.HsvToRgb(CurrentColorState.HSV_H, value / 255.0, CurrentColorState.HSV_V); + return Color.FromArgb(255, (byte)(rgb.R * 255), (byte)(rgb.G * 255), (byte)(rgb.B * 255)); } case "V": { - var rgbtuple = - ColorSpaceHelper.HsvToRgb(CurrentColorState.HSV_H, CurrentColorState.HSV_S, value / 255.0); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + var rgb = + RgbHelper.HsvToRgb(CurrentColorState.HSV_H, CurrentColorState.HSV_S, value / 255.0); + return Color.FromArgb(255, (byte)(rgb.R * 255), (byte)(rgb.G * 255), (byte)(rgb.B * 255)); } default: return Color.FromArgb((byte)(CurrentColorState.A * 255), (byte)(CurrentColorState.RGB_R * 255), diff --git a/src/ColorPicker.Models/ColorPicker.Models.csproj b/src/ColorPicker.Models/ColorPicker.Models.csproj index 7a90dcc..6370918 100644 --- a/src/ColorPicker.Models/ColorPicker.Models.csproj +++ b/src/ColorPicker.Models/ColorPicker.Models.csproj @@ -33,4 +33,5 @@ + diff --git a/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs b/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs index 0d2f85e..78c17a1 100644 --- a/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs +++ b/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs @@ -1,4 +1,5 @@ using System; +using ColorPicker.Models.Colors; namespace ColorPicker.Models.ColorSliders; @@ -18,11 +19,11 @@ public ColorSliderGradientPoint(double r, double g, double b, double position) Position = position; } - public ColorSliderGradientPoint(Tuple rgb, double position) + public ColorSliderGradientPoint(Rgb rgb, double position) { - R = rgb.Item1; - G = rgb.Item2; - B = rgb.Item3; + R = rgb.R; + G = rgb.G; + B = rgb.B; Position = position; } } \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/HslHelper.cs b/src/ColorPicker.Models/ColorSpaces/HslHelper.cs index 05768ae..aabb2d6 100644 --- a/src/ColorPicker.Models/ColorSpaces/HslHelper.cs +++ b/src/ColorPicker.Models/ColorSpaces/HslHelper.cs @@ -1,4 +1,5 @@ using System; +using ColorPicker.Models.Colors; namespace ColorPicker.Models.ColorSpaces; @@ -11,7 +12,7 @@ public static class HslHelper /// Blue channel /// Green channel /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Lightness (0-1) - public static Tuple RgbToHsl(double r, double g, double b) + public static Hsl RgbToHsl(double r, double g, double b) { double h, s, l; @@ -22,11 +23,11 @@ public static Tuple RgbToHsl(double r, double g, double if (max == 0) //pure black - return new Tuple(-1, -1, 0); + return new Hsl(-1, -1, 0); if (delta == 0) //gray - return new Tuple(-1, 0, l); + return new Hsl(-1, 0, l); //magic s = l <= 0.5 ? delta / (max + min) : delta / (2 - max - min); @@ -45,7 +46,7 @@ public static Tuple RgbToHsl(double r, double g, double h *= 360; - return new Tuple(h, s, l); + return new Hsl(h, s, l); } /// @@ -55,7 +56,7 @@ public static Tuple RgbToHsl(double r, double g, double /// Saturation, 0-1 /// Value, 0-1 /// Values in order: Hue (same), Saturation (0-1 or -1), Lightness (0-1) - public static Tuple HsvToHsl(double h, double s, double v) + public static Hsl HsvToHsl(double h, double s, double v) { var hsl_l = v * (1 - s / 2); double hsl_s; @@ -63,7 +64,7 @@ public static Tuple HsvToHsl(double h, double s, double hsl_s = -1; else hsl_s = (v - hsl_l) / Math.Min(hsl_l, 1 - hsl_l); - return new Tuple(h, hsl_s, hsl_l); + return new Hsl(h, hsl_s, hsl_l); } @@ -74,10 +75,10 @@ public static Tuple HsvToHsl(double h, double s, double /// Saturation, 0-1 /// Lightness, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) - public static Tuple OkHslToHsl(double h, double s, double l) + public static Hsl OkHslToHsl(double h, double s, double l) { var rgb = RgbHelper.OkHslToRgb(h, s, l); - return HslHelper.RgbToHsl(rgb.Item1, rgb.Item2, rgb.Item3); + return HslHelper.RgbToHsl(rgb.R, rgb.G, rgb.B); } /// @@ -87,9 +88,9 @@ public static Tuple OkHslToHsl(double h, double s, doubl /// Saturation, 0-1 /// Value, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) - public static Tuple OkHsvToHsl(double h, double s, double v) + public static Hsl OkHsvToHsl(double h, double s, double v) { var rgb = RgbHelper.OkHsvToRgb(h, s, v); - return HslHelper.RgbToHsl(rgb.Item1, rgb.Item2, rgb.Item3); + return HslHelper.RgbToHsl(rgb.R, rgb.G, rgb.B); } } \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs b/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs index 1994dad..4b21dbf 100644 --- a/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs +++ b/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs @@ -1,4 +1,5 @@ using System; +using ColorPicker.Models.Colors; namespace ColorPicker.Models.ColorSpaces; @@ -11,7 +12,7 @@ public static class HsvHelper /// Saturation, 0-1 /// Lightness, 0-1 /// Values in order: Hue (same), Saturation (0-1 or -1), Value (0-1) - public static Tuple HslToHsv(double h, double s, double l) + public static Hsv HslToHsv(double h, double s, double l) { var hsv_v = l + s * Math.Min(l, 1 - l); double hsv_s; @@ -19,7 +20,7 @@ public static Tuple HslToHsv(double h, double s, double hsv_s = -1; else hsv_s = 2 * (1 - l / hsv_v); - return new Tuple(h, hsv_s, hsv_v); + return new Hsv(h, hsv_s, hsv_v); } /// @@ -29,7 +30,7 @@ public static Tuple HslToHsv(double h, double s, double /// Green channel /// Blue channel /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Value (0-1) - public static Tuple RgbToHsv(double r, double g, double b) + public static Hsv RgbToHsv(double r, double g, double b) { double min, max, delta; double h, s, v; @@ -47,7 +48,7 @@ public static Tuple RgbToHsv(double r, double g, double //pure black s = -1; h = -1; - return new Tuple(h, s, v); + return new Hsv(h, s, v); } if (r == max) @@ -62,7 +63,7 @@ public static Tuple RgbToHsv(double r, double g, double if (double.IsNaN(h)) //delta == 0, case of pure gray h = -1; - return new Tuple(h, s, v); + return new Hsv(h, s, v); } /// @@ -72,10 +73,10 @@ public static Tuple RgbToHsv(double r, double g, double /// Saturation, 0-1 /// Lightness, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) - public static Tuple OkHslToHsv(double h, double s, double l) + public static Hsv OkHslToHsv(double h, double s, double l) { var rgb = RgbHelper.OkHslToRgb(h, s, l); - return HsvHelper.RgbToHsv(rgb.Item1, rgb.Item2, rgb.Item3); + return HsvHelper.RgbToHsv(rgb.R, rgb.G, rgb.B); } /// @@ -85,9 +86,9 @@ public static Tuple OkHslToHsv(double h, double s, doubl /// Saturation, 0-1 /// Value, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) - public static Tuple OkHsvToHsv(double h, double s, double v) + public static Hsv OkHsvToHsv(double h, double s, double v) { var rgb = RgbHelper.OkHsvToRgb(h, s, v); - return HsvHelper.RgbToHsv(rgb.Item1, rgb.Item2, rgb.Item3); + return HsvHelper.RgbToHsv(rgb.R, rgb.G, rgb.B); } } \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/OkHelper.cs b/src/ColorPicker.Models/ColorSpaces/OkHelper.cs index 74105fb..b4c476a 100644 --- a/src/ColorPicker.Models/ColorSpaces/OkHelper.cs +++ b/src/ColorPicker.Models/ColorSpaces/OkHelper.cs @@ -1,630 +1,561 @@ // Adapted from Björn Ottosson's C++ header https://bottosson.github.io/misc/ok_color.h using System; +using ColorPicker.Models.Colors; -namespace ColorPicker.Models.ColorSpaces +namespace ColorPicker.Models.ColorSpaces; + +public static class OkHelper { - public static class OkHelper + private struct LC { - private struct Lab - { - public double L; - public double a; - public double b; - }; + public double L; + public double C; + }; + + /// + /// Alternative representation of (L_cusp, C_cusp) + /// Encoded so S = C_cusp/L_cusp and T = C_cusp/(1-L_cusp) + /// The maximum value for C in the triangle is then found as fmin(S*L, T*(1-L)), for a given L + /// + private struct ST + { + public double S; + public double T; + }; - private struct RGB - { - public double r; - public double g; - public double b; - }; + private static double SrgbTransferFunction(double a) + { + return .0031308 >= a ? 12.92 * a : 1.055 * Math.Pow(a, .4166666666666667) - .055; + } - private struct HSV - { - public double h; - public double s; - public double v; - }; + private static double SrgbTransferFunctionInverse(double a) + { + return .04045 < a ? Math.Pow((a + .055) / 1.055, 2.4) : a / 12.92; + } - private struct HSL - { - public double h; - public double s; - public double l; - }; + private static Lab LinearSrgbToOklab(Rgb c) + { + double l = 0.4122214708 * c.R + 0.5363325363 * c.G + 0.0514459929 * c.B; + double m = 0.2119034982 * c.R + 0.6806995451 * c.G + 0.1073969566 * c.B; + double s = 0.0883024619 * c.R + 0.2817188376 * c.G + 0.6299787005 * c.B; - private struct LC - { - public double L; - public double C; - }; + double l_ = Math.Cbrt(l); + double m_ = Math.Cbrt(m); + double s_ = Math.Cbrt(s); - /// - /// Alternative representation of (L_cusp, C_cusp) - /// Encoded so S = C_cusp/L_cusp and T = C_cusp/(1-L_cusp) - /// The maximum value for C in the triangle is then found as fmin(S*L, T*(1-L)), for a given L - /// - private struct ST - { - public double S; - public double T; - }; + return new Lab(0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_); + } - private static double SrgbTransferFunction(double a) + private static Rgb OklabToLinearSrgb(Lab c) + { + double l_ = c.L + 0.3963377774 * c.a + 0.2158037573 * c.b; + double m_ = c.L - 0.1055613458 * c.a - 0.0638541728 * c.b; + double s_ = c.L - 0.0894841775 * c.a - 1.2914855480 * c.b; + + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; + + return new Rgb( + +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s); + } + + /// + /// Finds the maximum saturation possible for a given hue that fits in sRGB + /// Saturation here is defined as S = C/L + /// a and b must be normalized so a^2 + b^2 == 1 + /// + private static double ComputeMaxSaturation(double a, double b) + { + // Max saturation will be when one of r, g or b goes below zero. + + // Select different coefficients depending on which component goes below zero first + double k0, k1, k2, k3, k4, wl, wm, ws; + + if (-1.88170328 * a - 0.80936493 * b > 1) { - return .0031308 >= a ? 12.92 * a : 1.055 * Math.Pow(a, .4166666666666667) - .055; + // Red component + k0 = +1.19086277; + k1 = +1.76576728; + k2 = +0.59662641; + k3 = +0.75515197; + k4 = +0.56771245; + wl = +4.0767416621; + wm = -3.3077115913; + ws = +0.2309699292; } - - private static double SrgbTransferFunctionInverse(double a) + else if (1.81444104 * a - 1.19445276 * b > 1) { - return .04045 < a ? Math.Pow((a + .055) / 1.055, 2.4) : a / 12.92; + // Green component + k0 = +0.73956515; + k1 = -0.45954404; + k2 = +0.08285427; + k3 = +0.12541070; + k4 = +0.14503204; + wl = -1.2684380046; + wm = +2.6097574011; + ws = -0.3413193965; } - - private static Lab LinearSrgbToOklab(RGB c) + else { - double l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b; - double m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b; - double s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b; + // Blue component + k0 = +1.35733652; + k1 = -0.00915799; + k2 = -1.15130210; + k3 = -0.50559606; + k4 = +0.00692167; + wl = -0.0041960863; + wm = -0.7034186147; + ws = +1.7076147010; + } - double l_ = Math.Cbrt(l); - double m_ = Math.Cbrt(m); - double s_ = Math.Cbrt(s); + // Approximate max saturation using a polynomial: + double S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; - return new Lab() - { - L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, - a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, - b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_ - }; - } + // Do one step Halley's method to get closer + // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite + // this should be sufficient for most applications, otherwise do two/three steps + + double k_l = +0.3963377774 * a + 0.2158037573 * b; + double k_m = -0.1055613458 * a - 0.0638541728 * b; + double k_s = -0.0894841775 * a - 1.2914855480 * b; - private static RGB OklabToLinearSrgb(Lab c) { - double l_ = c.L + 0.3963377774 * c.a + 0.2158037573 * c.b; - double m_ = c.L - 0.1055613458 * c.a - 0.0638541728 * c.b; - double s_ = c.L - 0.0894841775 * c.a - 1.2914855480 * c.b; + double l_ = 1.0 + S * k_l; + double m_ = 1.0 + S * k_m; + double s_ = 1.0 + S * k_s; double l = l_ * l_ * l_; double m = m_ * m_ * m_; double s = s_ * s_ * s_; - return new RGB() - { - r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, - g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, - b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - }; - } - - /// - /// Finds the maximum saturation possible for a given hue that fits in sRGB - /// Saturation here is defined as S = C/L - /// a and b must be normalized so a^2 + b^2 == 1 - /// - private static double ComputeMaxSaturation(double a, double b) - { - // Max saturation will be when one of r, g or b goes below zero. - - // Select different coefficients depending on which component goes below zero first - double k0, k1, k2, k3, k4, wl, wm, ws; + double l_dS = 3.0 * k_l * l_ * l_; + double m_dS = 3.0 * k_m * m_ * m_; + double s_dS = 3.0 * k_s * s_ * s_; - if (-1.88170328 * a - 0.80936493 * b > 1) - { - // Red component - k0 = +1.19086277; - k1 = +1.76576728; - k2 = +0.59662641; - k3 = +0.75515197; - k4 = +0.56771245; - wl = +4.0767416621; - wm = -3.3077115913; - ws = +0.2309699292; - } - else if (1.81444104 * a - 1.19445276 * b > 1) - { - // Green component - k0 = +0.73956515; - k1 = -0.45954404; - k2 = +0.08285427; - k3 = +0.12541070; - k4 = +0.14503204; - wl = -1.2684380046; - wm = +2.6097574011; - ws = -0.3413193965; - } - else - { - // Blue component - k0 = +1.35733652; - k1 = -0.00915799; - k2 = -1.15130210; - k3 = -0.50559606; - k4 = +0.00692167; - wl = -0.0041960863; - wm = -0.7034186147; - ws = +1.7076147010; - } - - // Approximate max saturation using a polynomial: - double S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; - - // Do one step Halley's method to get closer - // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite - // this should be sufficient for most applications, otherwise do two/three steps + double l_dS2 = 6.0 * k_l * k_l * l_; + double m_dS2 = 6.0 * k_m * k_m * m_; + double s_dS2 = 6.0 * k_s * k_s * s_; - double k_l = +0.3963377774 * a + 0.2158037573 * b; - double k_m = -0.1055613458 * a - 0.0638541728 * b; - double k_s = -0.0894841775 * a - 1.2914855480 * b; + double f = wl * l + wm * m + ws * s; + double f1 = wl * l_dS + wm * m_dS + ws * s_dS; + double f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; - { - double l_ = 1.0 + S * k_l; - double m_ = 1.0 + S * k_m; - double s_ = 1.0 + S * k_s; + S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); + } - double l = l_ * l_ * l_; - double m = m_ * m_ * m_; - double s = s_ * s_ * s_; + return S; + } - double l_dS = 3.0 * k_l * l_ * l_; - double m_dS = 3.0 * k_m * m_ * m_; - double s_dS = 3.0 * k_s * s_ * s_; + /// + /// finds L_cusp and C_cusp for a given hue + /// a and b must be normalized so a^2 + b^2 == 1 + /// + private static LC FindCusp(double a, double b) + { + // First, find the maximum saturation (saturation S = C/L) + double S_cusp = ComputeMaxSaturation(a, b); - double l_dS2 = 6.0 * k_l * k_l * l_; - double m_dS2 = 6.0 * k_m * k_m * m_; - double s_dS2 = 6.0 * k_s * k_s * s_; + // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: + Rgb rgb_at_max = OklabToLinearSrgb(new Lab(1, S_cusp * a, S_cusp * b)); + + double L_cusp = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_at_max.R, rgb_at_max.G), rgb_at_max.B)); + double C_cusp = L_cusp * S_cusp; - double f = wl * l + wm * m + ws * s; - double f1 = wl * l_dS + wm * m_dS + ws * s_dS; - double f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; + return new LC + { + L = L_cusp, + C = C_cusp + }; + } - S = S - f * f1 / (f1 * f1 - 0.5 * f * f2); - } + /// + /// Finds intersection of the line defined by + /// L = L0 * (1 - t) + t * L1; + /// C = t * C1; + /// a and b must be normalized so a^2 + b^2 == 1 + /// + private static double FindGamutIntersection(double a, double b, double L1, double C1, double L0, LC cusp) + { + // Find the intersection for upper and lower half seprately + double t; + if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.0) + { + // Lower half - return S; + t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); } - - /// - /// finds L_cusp and C_cusp for a given hue - /// a and b must be normalized so a^2 + b^2 == 1 - /// - private static LC FindCusp(double a, double b) + else { - // First, find the maximum saturation (saturation S = C/L) - double S_cusp = ComputeMaxSaturation(a, b); + // Upper half - // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: - RGB rgb_at_max = OklabToLinearSrgb(new Lab() - { - L = 1, - a = S_cusp * a, - b = S_cusp * b - }); - - double L_cusp = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_at_max.r, rgb_at_max.g), rgb_at_max.b)); - double C_cusp = L_cusp * S_cusp; + // First intersect with triangle + t = cusp.C * (L0 - 1.0) / (C1 * (cusp.L - 1.0) + cusp.C * (L0 - L1)); - return new LC + // Then one step Halley's method { - L = L_cusp, - C = C_cusp - }; - } + double dL = L1 - L0; + double dC = C1; - /// - /// Finds intersection of the line defined by - /// L = L0 * (1 - t) + t * L1; - /// C = t * C1; - /// a and b must be normalized so a^2 + b^2 == 1 - /// - private static double FindGamutIntersection(double a, double b, double L1, double C1, double L0, LC cusp) - { - // Find the intersection for upper and lower half seprately - double t; - if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.0) - { - // Lower half + double k_l = +0.3963377774 * a + 0.2158037573 * b; + double k_m = -0.1055613458 * a - 0.0638541728 * b; + double k_s = -0.0894841775 * a - 1.2914855480 * b; - t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); - } - else - { - // Upper half + double l_dt = dL + dC * k_l; + double m_dt = dL + dC * k_m; + double s_dt = dL + dC * k_s; - // First intersect with triangle - t = cusp.C * (L0 - 1.0) / (C1 * (cusp.L - 1.0) + cusp.C * (L0 - L1)); - // Then one step Halley's method + // If higher accuracy is required, 2 or 3 iterations of the following block can be used: { - double dL = L1 - L0; - double dC = C1; - - double k_l = +0.3963377774 * a + 0.2158037573 * b; - double k_m = -0.1055613458 * a - 0.0638541728 * b; - double k_s = -0.0894841775 * a - 1.2914855480 * b; + double L = L0 * (1.0 - t) + t * L1; + double C = t * C1; - double l_dt = dL + dC * k_l; - double m_dt = dL + dC * k_m; - double s_dt = dL + dC * k_s; + double l_ = L + C * k_l; + double m_ = L + C * k_m; + double s_ = L + C * k_s; + double l = l_ * l_ * l_; + double m = m_ * m_ * m_; + double s = s_ * s_ * s_; - // If higher accuracy is required, 2 or 3 iterations of the following block can be used: - { - double L = L0 * (1.0 - t) + t * L1; - double C = t * C1; + double ldt = 3 * l_dt * l_ * l_; + double mdt = 3 * m_dt * m_ * m_; + double sdt = 3 * s_dt * s_ * s_; - double l_ = L + C * k_l; - double m_ = L + C * k_m; - double s_ = L + C * k_s; + double ldt2 = 6 * l_dt * l_dt * l_; + double mdt2 = 6 * m_dt * m_dt * m_; + double sdt2 = 6 * s_dt * s_dt * s_; - double l = l_ * l_ * l_; - double m = m_ * m_ * m_; - double s = s_ * s_ * s_; + double r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1; + double r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; + double r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; - double ldt = 3 * l_dt * l_ * l_; - double mdt = 3 * m_dt * m_ * m_; - double sdt = 3 * s_dt * s_ * s_; + double u_r = r1 / (r1 * r1 - 0.5 * r * r2); + double t_r = -r * u_r; - double ldt2 = 6 * l_dt * l_dt * l_; - double mdt2 = 6 * m_dt * m_dt * m_; - double sdt2 = 6 * s_dt * s_dt * s_; + double g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1; + double g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; + double g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; - double r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s - 1; - double r1 = 4.0767416621 * ldt - 3.3077115913 * mdt + 0.2309699292 * sdt; - double r2 = 4.0767416621 * ldt2 - 3.3077115913 * mdt2 + 0.2309699292 * sdt2; + double u_g = g1 / (g1 * g1 - 0.5 * g * g2); + double t_g = -g * u_g; - double u_r = r1 / (r1 * r1 - 0.5 * r * r2); - double t_r = -r * u_r; + double _b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1; + double b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; + double b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; - double g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s - 1; - double g1 = -1.2684380046 * ldt + 2.6097574011 * mdt - 0.3413193965 * sdt; - double g2 = -1.2684380046 * ldt2 + 2.6097574011 * mdt2 - 0.3413193965 * sdt2; + double u_b = b1 / (b1 * b1 - 0.5 * _b * b2); + double t_b = -_b * u_b; - double u_g = g1 / (g1 * g1 - 0.5 * g * g2); - double t_g = -g * u_g; + t_r = u_r >= 0.0 ? t_r : float.MaxValue; + t_g = u_g >= 0.0 ? t_g : float.MaxValue; + t_b = u_b >= 0.0 ? t_b : float.MaxValue; - double _b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s - 1; - double b1 = -0.0041960863 * ldt - 0.7034186147 * mdt + 1.7076147010 * sdt; - double b2 = -0.0041960863 * ldt2 - 0.7034186147 * mdt2 + 1.7076147010 * sdt2; - - double u_b = b1 / (b1 * b1 - 0.5 * _b * b2); - double t_b = -_b * u_b; - - t_r = u_r >= 0.0 ? t_r : float.MaxValue; - t_g = u_g >= 0.0 ? t_g : float.MaxValue; - t_b = u_b >= 0.0 ? t_b : float.MaxValue; - - t += Math.Min(t_r, Math.Min(t_g, t_b)); - } + t += Math.Min(t_r, Math.Min(t_g, t_b)); } } - - return t; } - private static double FindGamutIntersection(double a, double b, double L1, double C1, double L0) - { - // Find the cusp of the gamut triangle - LC cusp = FindCusp(a, b); + return t; + } - return FindGamutIntersection(a, b, L1, C1, L0, cusp); - } + private static double FindGamutIntersection(double a, double b, double L1, double C1, double L0) + { + // Find the cusp of the gamut triangle + LC cusp = FindCusp(a, b); - private static double Toe(double x) - { - const double k_1 = 0.206; - const double k_2 = 0.03; - const double k_3 = (1.0 + k_1) / (1.0 + k_2); - return 0.5 * (k_3 * x - k_1 + Math.Sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x)); - } + return FindGamutIntersection(a, b, L1, C1, L0, cusp); + } - private static double ToeInverse(double x) - { - const double k_1 = 0.206; - const double k_2 = 0.03; - const double k_3 = (1.0 + k_1) / (1.0 + k_2); - return (x * x + k_1 * x) / (k_3 * (x + k_2)); - } + private static double Toe(double x) + { + const double k_1 = 0.206; + const double k_2 = 0.03; + const double k_3 = (1.0 + k_1) / (1.0 + k_2); + return 0.5 * (k_3 * x - k_1 + Math.Sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x)); + } - private static ST ToSt(LC cusp) - { - double L = cusp.L; - double C = cusp.C; - return new ST() - { - S = C / L, - T = C / (1 - L) - }; - } + private static double ToeInverse(double x) + { + const double k_1 = 0.206; + const double k_2 = 0.03; + const double k_3 = (1.0 + k_1) / (1.0 + k_2); + return (x * x + k_1 * x) / (k_3 * (x + k_2)); + } - /// - /// Returns a smooth approximation of the location of the cusp - /// This polynomial was created by an optimization process - /// It has been designed so that S_mid < S_max and T_mid < T_max - /// - private static ST GetStMid(double a_, double b_) + private static ST ToSt(LC cusp) + { + double L = cusp.L; + double C = cusp.C; + return new ST() { - double S = 0.11516993 + 1.0 / ( - +7.44778970 + 4.15901240 * b_ - + a_ * (-2.19557347 + 1.75198401 * b_ - + a_ * (-2.13704948 - 10.02301043 * b_ - + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_ - ))) - ); - - double T = 0.11239642 + 1.0 / ( - +1.61320320 - 0.68124379 * b_ - + a_ * (+0.40370612 + 0.90148123 * b_ - + a_ * (-0.27087943 + 0.61223990 * b_ - + a_ * (+0.00299215 - 0.45399568 * b_ - 0.14661872 * a_ - ))) - ); - - return new ST() - { - S = S, - T = T - }; - } + S = C / L, + T = C / (1 - L) + }; + } - struct Cs + /// + /// Returns a smooth approximation of the location of the cusp + /// This polynomial was created by an optimization process + /// It has been designed so that S_mid < S_max and T_mid < T_max + /// + private static ST GetStMid(double a_, double b_) + { + double S = 0.11516993 + 1.0 / ( + +7.44778970 + 4.15901240 * b_ + + a_ * (-2.19557347 + 1.75198401 * b_ + + a_ * (-2.13704948 - 10.02301043 * b_ + + a_ * (-4.24894561 + 5.38770819 * b_ + 4.69891013 * a_ + ))) + ); + + double T = 0.11239642 + 1.0 / ( + +1.61320320 - 0.68124379 * b_ + + a_ * (+0.40370612 + 0.90148123 * b_ + + a_ * (-0.27087943 + 0.61223990 * b_ + + a_ * (+0.00299215 - 0.45399568 * b_ - 0.14661872 * a_ + ))) + ); + + return new ST() { - public double C_0; - public double C_mid; - public double C_max; + S = S, + T = T }; + } - private static Cs GetCs(double L, double a_, double b_) - { - LC cusp = FindCusp(a_, b_); + struct Cs + { + public double C_0; + public double C_mid; + public double C_max; + }; - double C_max = FindGamutIntersection(a_, b_, L, 1, L, cusp); - ST ST_max = ToSt(cusp); + private static Cs GetCs(double L, double a_, double b_) + { + LC cusp = FindCusp(a_, b_); - // Scale factor to compensate for the curved part of gamut shape: - double k = C_max / Math.Min((L * ST_max.S), (1 - L) * ST_max.T); + double C_max = FindGamutIntersection(a_, b_, L, 1, L, cusp); + ST ST_max = ToSt(cusp); - double C_mid; - { - ST ST_mid = GetStMid(a_, b_); + // Scale factor to compensate for the curved part of gamut shape: + double k = C_max / Math.Min((L * ST_max.S), (1 - L) * ST_max.T); - // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. - double C_a = L * ST_mid.S; - double C_b = (1.0 - L) * ST_mid.T; - C_mid = 0.9 * k * Math.Sqrt(Math.Sqrt(1.0 / (1.0 / (C_a * C_a * C_a * C_a) + 1.0 / (C_b * C_b * C_b * C_b)))); - } + double C_mid; + { + ST ST_mid = GetStMid(a_, b_); - double C_0; - { - // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST. - double C_a = L * 0.4; - double C_b = (1.0 - L) * 0.8; + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + double C_a = L * ST_mid.S; + double C_b = (1.0 - L) * ST_mid.T; + C_mid = 0.9 * k * Math.Sqrt(Math.Sqrt(1.0 / (1.0 / (C_a * C_a * C_a * C_a) + 1.0 / (C_b * C_b * C_b * C_b)))); + } - // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. - C_0 = Math.Sqrt(1.0 / (1.0 / (C_a * C_a) + 1.0 / (C_b * C_b))); - } + double C_0; + { + // for C_0, the shape is independent of hue, so ST are constant. Values picked to roughly be the average values of ST. + double C_a = L * 0.4; + double C_b = (1.0 - L) * 0.8; - return new Cs() - { - C_0 = C_0, - C_mid = C_mid, - C_max = C_max - }; + // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. + C_0 = Math.Sqrt(1.0 / (1.0 / (C_a * C_a) + 1.0 / (C_b * C_b))); } - public static Tuple OkHslToSrgb(double h, double s, double l) + return new Cs() { - if (l == 1.0) - { - return new Tuple(1.0, 1.0, 1.0); - } - else if (l == 0.0) - { - return new Tuple(0.0, 0.0, 0.0); - } + C_0 = C_0, + C_mid = C_mid, + C_max = C_max + }; + } - double a_ = Math.Cos(2.0 * Math.PI * h); - double b_ = Math.Sin(2.0 * Math.PI * h); - double L = ToeInverse(l); + public static Rgb OkHslToSrgb(double h, double s, double l) + { + if (l == 1.0) + { + return new Rgb(1.0, 1.0, 1.0); + } + else if (l == 0.0) + { + return new Rgb(0.0, 0.0, 0.0); + } - Cs cs = GetCs(L, a_, b_); - double C_0 = cs.C_0; - double C_mid = cs.C_mid; - double C_max = cs.C_max; + double a_ = Math.Cos(2.0 * Math.PI * h); + double b_ = Math.Sin(2.0 * Math.PI * h); + double L = ToeInverse(l); - double mid = 0.8; - double mid_inv = 1.25; + Cs cs = GetCs(L, a_, b_); + double C_0 = cs.C_0; + double C_mid = cs.C_mid; + double C_max = cs.C_max; - double C, t, k_0, k_1, k_2; + double mid = 0.8; + double mid_inv = 1.25; - if (s < mid) - { - t = mid_inv * s; + double C, t, k_0, k_1, k_2; - k_1 = mid * C_0; - k_2 = (1.0 - k_1 / C_mid); + if (s < mid) + { + t = mid_inv * s; - C = t * k_1 / (1.0 - k_2 * t); - } - else - { - t = (s - mid) / (1 - mid); + k_1 = mid * C_0; + k_2 = (1.0 - k_1 / C_mid); - k_0 = C_mid; - k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; - k_2 = (1.0 - (k_1) / (C_max - C_mid)); + C = t * k_1 / (1.0 - k_2 * t); + } + else + { + t = (s - mid) / (1 - mid); - C = k_0 + t * k_1 / (1.0 - k_2 * t); - } + k_0 = C_mid; + k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + k_2 = (1.0 - (k_1) / (C_max - C_mid)); - RGB rgb = OklabToLinearSrgb(new Lab() - { - L = L, - a = C * a_, - b= C * b_ - }); - - return new Tuple( - SrgbTransferFunction(rgb.r), - SrgbTransferFunction(rgb.g), - SrgbTransferFunction(rgb.b) - ); + C = k_0 + t * k_1 / (1.0 - k_2 * t); } - public static Tuple SrgbToOkHsl(double r, double g, double b) - { - Lab lab = LinearSrgbToOklab(new RGB() - { - r = SrgbTransferFunctionInverse(r), - g = SrgbTransferFunctionInverse(g), - b = SrgbTransferFunctionInverse(b) - }); + Rgb rgb = OklabToLinearSrgb(new Lab(L, C * a_, C * b_)); - double C = Math.Sqrt(lab.a * lab.a + lab.b * lab.b); - double a_ = lab.a / C; - double b_ = lab.b / C; + return new Rgb( + SrgbTransferFunction(rgb.R), + SrgbTransferFunction(rgb.G), + SrgbTransferFunction(rgb.B) + ); + } - double L = lab.L; - double h = 0.5 + 0.5 * Math.Atan2(-lab.b, -lab.a) / Math.PI; + public static Hsl SrgbToOkHsl(double r, double g, double b) + { + Lab lab = LinearSrgbToOklab(new Rgb(SrgbTransferFunctionInverse(r), SrgbTransferFunctionInverse(g), SrgbTransferFunctionInverse(b))); - Cs cs = GetCs(L, a_, b_); - double C_0 = cs.C_0; - double C_mid = cs.C_mid; - double C_max = cs.C_max; + double C = Math.Sqrt(lab.a * lab.a + lab.b * lab.b); + double a_ = lab.a / C; + double b_ = lab.b / C; - // Inverse of the interpolation in OkHslToSrgb: + double L = lab.L; + double h = 0.5 + 0.5 * Math.Atan2(-lab.b, -lab.a) / Math.PI; - double mid = 0.8; - double mid_inv = 1.25; + Cs cs = GetCs(L, a_, b_); + double C_0 = cs.C_0; + double C_mid = cs.C_mid; + double C_max = cs.C_max; - double s; - if (C < C_mid) - { - double k_1 = mid * C_0; - double k_2 = (1.0 - k_1 / C_mid); + // Inverse of the interpolation in OkHslToSrgb: - double t = C / (k_1 + k_2 * C); - s = t * mid; - } - else - { - double k_0 = C_mid; - double k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; - double k_2 = (1.0 - (k_1) / (C_max - C_mid)); + double mid = 0.8; + double mid_inv = 1.25; - double t = (C - k_0) / (k_1 + k_2 * (C - k_0)); - s = mid + (1.0 - mid) * t; - } + double s; + if (C < C_mid) + { + double k_1 = mid * C_0; + double k_2 = (1.0 - k_1 / C_mid); - double l = Toe(L); - return new Tuple( - h, - s, - l - ); + double t = C / (k_1 + k_2 * C); + s = t * mid; } + else + { + double k_0 = C_mid; + double k_1 = (1.0 - mid) * C_mid * C_mid * mid_inv * mid_inv / C_0; + double k_2 = (1.0 - (k_1) / (C_max - C_mid)); + double t = (C - k_0) / (k_1 + k_2 * (C - k_0)); + s = mid + (1.0 - mid) * t; + } - public static Tuple OkHsvToSrgb(double h, double s, double v) - { - double a_ = Math.Cos(2.0 * Math.PI * h); - double b_ = Math.Sin(2.0 * Math.PI * h); + return new Hsl(h, s, Toe(L)); + } - LC cusp = FindCusp(a_, b_); - ST ST_max = ToSt(cusp); - double S_max = ST_max.S; - double T_max = ST_max.T; - double S_0 = 0.5; - double k = 1 - S_0 / S_max; - // first we compute L and V as if the gamut is a perfect triangle: + public static Rgb OkHsvToSrgb(double h, double s, double v) + { + double a_ = Math.Cos(2.0 * Math.PI * h); + double b_ = Math.Sin(2.0 * Math.PI * h); - // L, C when v==1: - double L_v = 1 - s * S_0 / (S_0 + T_max - T_max * k * s); - double C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); + LC cusp = FindCusp(a_, b_); + ST ST_max = ToSt(cusp); + double S_max = ST_max.S; + double T_max = ST_max.T; + double S_0 = 0.5; + double k = 1 - S_0 / S_max; - double L = v * L_v; - double C = v * C_v; + // first we compute L and V as if the gamut is a perfect triangle: - // then we compensate for both Toe and the curved top part of the triangle: - double L_vt = ToeInverse(L_v); - double C_vt = C_v * L_vt / L_v; + // L, C when v==1: + double L_v = 1 - s * S_0 / (S_0 + T_max - T_max * k * s); + double C_v = s * T_max * S_0 / (S_0 + T_max - T_max * k * s); - double L_new = ToeInverse(L); - C = C * L_new / L; - L = L_new; + double L = v * L_v; + double C = v * C_v; - RGB rgb_scale = OklabToLinearSrgb(new Lab() - { - L = L_vt, a = a_* C_vt, b = b_ *C_vt - }); - double scale_L = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_scale.r, rgb_scale.g), Math.Max(rgb_scale.b, 0.0))); + // then we compensate for both Toe and the curved top part of the triangle: + double L_vt = ToeInverse(L_v); + double C_vt = C_v * L_vt / L_v; - L = L * scale_L; - C = C * scale_L; + double L_new = ToeInverse(L); + C = C * L_new / L; + L = L_new; - RGB rgb = OklabToLinearSrgb( new Lab() - { - L = L, a = C * a_, b = C * b_ - }); + Rgb rgb_scale = OklabToLinearSrgb(new Lab(L_vt, a_ * C_vt, b_ * C_vt)); + double scale_L = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_scale.R, rgb_scale.G), Math.Max(rgb_scale.B, 0.0))); + + L = L * scale_L; + C = C * scale_L; + + Rgb rgb = OklabToLinearSrgb(new Lab(L, C * a_, C * b_)); - return new Tuple( - SrgbTransferFunction(rgb.r), - SrgbTransferFunction(rgb.g), - SrgbTransferFunction(rgb.b) - ); - } + return new Rgb( + SrgbTransferFunction(rgb.R), + SrgbTransferFunction(rgb.G), + SrgbTransferFunction(rgb.B) + ); + } - public static Tuple SrgbToOkHsv(double r, double g, double b) - { - Lab lab = LinearSrgbToOklab(new RGB() - { - r = SrgbTransferFunctionInverse(r), - g = SrgbTransferFunctionInverse(g), - b = SrgbTransferFunctionInverse(b) - }); + public static Hsv SrgbToOkHsv(double r, double g, double b) + { + Lab lab = LinearSrgbToOklab(new Rgb(SrgbTransferFunctionInverse(r), SrgbTransferFunctionInverse(g), SrgbTransferFunctionInverse(b))); - double C = Math.Sqrt(lab.a * lab.a + lab.b * lab.b); - double a_ = lab.a / C; - double b_ = lab.b / C; + double C = Math.Sqrt(lab.a * lab.a + lab.b * lab.b); + double a_ = lab.a / C; + double b_ = lab.b / C; - double L = lab.L; - double h = 0.5 + 0.5 * Math.Atan2(-lab.b, -lab.a) / Math.PI; + double L = lab.L; + double h = 0.5 + 0.5 * Math.Atan2(-lab.b, -lab.a) / Math.PI; - LC cusp = FindCusp(a_, b_); - ST ST_max = ToSt(cusp); - double S_max = ST_max.S; - double T_max = ST_max.T; - double S_0 = 0.5; - double k = 1 - S_0 / S_max; + LC cusp = FindCusp(a_, b_); + ST ST_max = ToSt(cusp); + double S_max = ST_max.S; + double T_max = ST_max.T; + double S_0 = 0.5; + double k = 1 - S_0 / S_max; - // first we find L_v, C_v, L_vt and C_vt + // first we find L_v, C_v, L_vt and C_vt - double t = T_max / (C + L * T_max); - double L_v = t * L; - double C_v = t * C; + double t = T_max / (C + L * T_max); + double L_v = t * L; + double C_v = t * C; - double L_vt = ToeInverse(L_v); - double C_vt = C_v * L_vt / L_v; + double L_vt = ToeInverse(L_v); + double C_vt = C_v * L_vt / L_v; - // we can then use these to invert the step that compensates for the Toe and the curved top part of the triangle: - RGB rgb_scale = OklabToLinearSrgb(new Lab() - { - L = L_vt, a = a_* C_vt, b = b_ *C_vt - }); - double scale_L = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_scale.r, rgb_scale.g), Math.Max(rgb_scale.b, 0.0))); + // we can then use these to invert the step that compensates for the Toe and the curved top part of the triangle: + Rgb rgb_scale = OklabToLinearSrgb(new Lab(L_vt, a_ * C_vt, b_ * C_vt)); + double scale_L = Math.Cbrt(1.0 / Math.Max(Math.Max(rgb_scale.R, rgb_scale.G), Math.Max(rgb_scale.B, 0.0))); - L = L / scale_L; - C = C / scale_L; + L = L / scale_L; + C = C / scale_L; - C = C * Toe(L) / L; - L = Toe(L); + C = C * Toe(L) / L; + L = Toe(L); - // we can now compute v and s: + // we can now compute v and s: - double v = L / L_v; - double s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); + double v = L / L_v; + double s = (S_0 + T_max) * C_v / ((T_max * S_0) + T_max * k * C_v); - return new Tuple(h, s, v); - } + return new Hsv(h, s, v); } } \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs b/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs index e70f4e9..5d301e4 100644 --- a/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs +++ b/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs @@ -1,4 +1,5 @@ using System; +using ColorPicker.Models.Colors; namespace ColorPicker.Models.ColorSpaces; @@ -11,10 +12,10 @@ public static class OkHslHelper /// Blue channel /// Green channel /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Lightness (0-1) - public static Tuple RgbToOkHsl(double r, double g, double b) + public static Hsl RgbToOkHsl(double r, double g, double b) { - var tuple = OkHelper.SrgbToOkHsl(r, g, b); - return new Tuple(tuple.Item1 * 360, tuple.Item2, tuple.Item3); + var hsl = OkHelper.SrgbToOkHsl(r, g, b); + return new Hsl(hsl.H * 360, hsl.S, hsl.L); } /// @@ -24,10 +25,10 @@ public static Tuple RgbToOkHsl(double r, double g, doubl /// Saturation, 0-1 /// Lightness, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) - public static Tuple HslToOkHsl(double h, double s, double l) + public static Hsl HslToOkHsl(double h, double s, double l) { var rgb = RgbHelper.HslToRgb(h, s, l); - return OkHslHelper.RgbToOkHsl(rgb.Item1, rgb.Item2, rgb.Item3); + return OkHslHelper.RgbToOkHsl(rgb.R, rgb.G, rgb.B); } /// @@ -37,10 +38,10 @@ public static Tuple HslToOkHsl(double h, double s, doubl /// Saturation, 0-1 /// Value, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) - public static Tuple HsvToOkHsl(double h, double s, double v) + public static Hsl HsvToOkHsl(double h, double s, double v) { var rgb = RgbHelper.HsvToRgb(h, s, v); - return OkHslHelper.RgbToOkHsl(rgb.Item1, rgb.Item2, rgb.Item3); + return OkHslHelper.RgbToOkHsl(rgb.R, rgb.G, rgb.B); } /// @@ -50,9 +51,9 @@ public static Tuple HsvToOkHsl(double h, double s, doubl /// Saturation, 0-1 /// Value, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Lightness (0-1) - public static Tuple OkHsvToOkHsl(double h, double s, double v) + public static Hsl OkHsvToOkHsl(double h, double s, double v) { var rgb = RgbHelper.OkHsvToRgb(h, s, v); - return OkHslHelper.RgbToOkHsl(rgb.Item1, rgb.Item2, rgb.Item3); + return OkHslHelper.RgbToOkHsl(rgb.R, rgb.G, rgb.B); } } \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs b/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs index fc9e122..7272de3 100644 --- a/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs +++ b/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs @@ -1,4 +1,5 @@ using System; +using ColorPicker.Models.Colors; namespace ColorPicker.Models.ColorSpaces; @@ -11,10 +12,10 @@ public static class OkHsvHelper /// Green channel /// Blue channel /// Values in order: Hue (0-360 or -1), Saturation (0-1 or -1), Value (0-1) - public static Tuple RgbToOkHsv(double r, double g, double b) + public static Hsv RgbToOkHsv(double r, double g, double b) { - var tuple = OkHelper.SrgbToOkHsl(r, g, b); - return new Tuple(tuple.Item1 * 360, tuple.Item2, tuple.Item3); + var hsl = OkHelper.SrgbToOkHsl(r, g, b); + return new Hsv(hsl.H * 360, hsl.S, hsl.L); } /// @@ -24,10 +25,10 @@ public static Tuple RgbToOkHsv(double r, double g, doubl /// Saturation, 0-1 /// Lightness, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) - public static Tuple HslToOkHsv(double h, double s, double l) + public static Hsv HslToOkHsv(double h, double s, double l) { var rgb = RgbHelper.HslToRgb(h, s, l); - return OkHsvHelper.RgbToOkHsv(rgb.Item1, rgb.Item2, rgb.Item3); + return OkHsvHelper.RgbToOkHsv(rgb.R, rgb.G, rgb.B); } /// @@ -37,10 +38,10 @@ public static Tuple HslToOkHsv(double h, double s, doubl /// Saturation, 0-1 /// Value, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) - public static Tuple HsvToOkHsv(double h, double s, double v) + public static Hsv HsvToOkHsv(double h, double s, double v) { var rgb = RgbHelper.HsvToRgb(h, s, v); - return OkHsvHelper.RgbToOkHsv(rgb.Item1, rgb.Item2, rgb.Item3); + return OkHsvHelper.RgbToOkHsv(rgb.R, rgb.G, rgb.B); } /// @@ -50,9 +51,9 @@ public static Tuple HsvToOkHsv(double h, double s, doubl /// Saturation, 0-1 /// Lightness, 0-1 /// Values in order: Hue (0-360), Saturation (0-1), Value (0-1) - public static Tuple OkHslToOkHsv(double h, double s, double l) + public static Hsv OkHslToOkHsv(double h, double s, double l) { var rgb = RgbHelper.OkHslToRgb(h, s, l); - return OkHsvHelper.RgbToOkHsv(rgb.Item1, rgb.Item2, rgb.Item3); + return OkHsvHelper.RgbToOkHsv(rgb.R, rgb.G, rgb.B); } } \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs b/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs index a181f9b..2ebe90c 100644 --- a/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs +++ b/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs @@ -1,4 +1,5 @@ using System; +using ColorPicker.Models.Colors; namespace ColorPicker.Models.ColorSpaces; @@ -11,11 +12,11 @@ public static class RgbHelper /// Saturation, 0-1 /// Value, 0-1 /// Values (0-1) in order: R, G, B - public static Tuple HsvToRgb(double h, double s, double v) + public static Rgb HsvToRgb(double h, double s, double v) { if (s == 0) // achromatic (grey) - return new Tuple(v, v, v); + return new Rgb(v, v, v); if (h >= 360.0) h = 0; h /= 60; @@ -27,12 +28,12 @@ public static Tuple HsvToRgb(double h, double s, double switch (i) { - case 0: return new Tuple(v, t, p); - case 1: return new Tuple(q, v, p); - case 2: return new Tuple(p, v, t); - case 3: return new Tuple(p, q, v); - case 4: return new Tuple(t, p, v); - default: return new Tuple(v, p, q); + case 0: return new Rgb(v, t, p); + case 1: return new Rgb(q, v, p); + case 2: return new Rgb(p, v, t); + case 3: return new Rgb(p, q, v); + case 4: return new Rgb(t, p, v); + default: return new Rgb(v, p, q); } } @@ -43,7 +44,7 @@ public static Tuple HsvToRgb(double h, double s, double /// Saturation, 0-1 /// Lightness, 0-1 /// Values (0-1) in order: R, G, B - public static Tuple HslToRgb(double h, double s, double l) + public static Rgb HslToRgb(double h, double s, double l) { var hueCircleSegment = (int)(h / 60); var circleSegmentFraction = (h - 60 * hueCircleSegment) / 60; @@ -55,22 +56,22 @@ public static Tuple HslToRgb(double h, double s, double switch (hueCircleSegment) { case 0: - return new Tuple(maxRGB, delta * circleSegmentFraction + minRGB, + return new Rgb(maxRGB, delta * circleSegmentFraction + minRGB, minRGB); //red-yellow case 1: - return new Tuple(delta * (1 - circleSegmentFraction) + minRGB, maxRGB, + return new Rgb(delta * (1 - circleSegmentFraction) + minRGB, maxRGB, minRGB); //yellow-green case 2: - return new Tuple(minRGB, maxRGB, + return new Rgb(minRGB, maxRGB, delta * circleSegmentFraction + minRGB); //green-cyan case 3: - return new Tuple(minRGB, delta * (1 - circleSegmentFraction) + minRGB, + return new Rgb(minRGB, delta * (1 - circleSegmentFraction) + minRGB, maxRGB); //cyan-blue case 4: - return new Tuple(delta * circleSegmentFraction + minRGB, minRGB, + return new Rgb(delta * circleSegmentFraction + minRGB, minRGB, maxRGB); //blue-purple default: - return new Tuple(maxRGB, minRGB, + return new Rgb(maxRGB, minRGB, delta * (1 - circleSegmentFraction) + minRGB); //purple-red and invalid values } } @@ -82,10 +83,10 @@ public static Tuple HslToRgb(double h, double s, double /// Saturation, 0-1 /// Value, 0-1 /// Values (0-1) in order: R, G, B - public static Tuple OkHsvToRgb(double h, double s, double v) + public static Rgb OkHsvToRgb(double h, double s, double v) { - var tuple = OkHelper.OkHsvToSrgb(h / 360, s, v); - return new Tuple(tuple.Item1, tuple.Item2, tuple.Item3); + var rgb = OkHelper.OkHsvToSrgb(h / 360, s, v); + return new Rgb(rgb.R, rgb.G, rgb.B); } /// @@ -95,9 +96,9 @@ public static Tuple OkHsvToRgb(double h, double s, doubl /// Saturation, 0-1 /// Lightness, 0-1 /// Values (0-1) in order: R, G, B - public static Tuple OkHslToRgb(double h, double s, double l) + public static Rgb OkHslToRgb(double h, double s, double l) { - var tuple = OkHelper.OkHslToSrgb(h / 360.0, s, l); - return new Tuple(tuple.Item1, tuple.Item2, tuple.Item3); + var rgb = OkHelper.OkHslToSrgb(h / 360.0, s, l); + return new Rgb(rgb.R, rgb.G, rgb.B); } } \ No newline at end of file diff --git a/src/ColorPicker.Models/ColorState.cs b/src/ColorPicker.Models/ColorState.cs index cfbefef..12ad551 100644 --- a/src/ColorPicker.Models/ColorState.cs +++ b/src/ColorPicker.Models/ColorState.cs @@ -265,7 +265,7 @@ public double OKHSL_L private void RecalculateHSLFromRGB() { var hsltuple = HslHelper.RgbToHsl(_RGB_R, _RGB_G, _RGB_B); - double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; + double h = hsltuple.H, s = hsltuple.S, l = hsltuple.L; if (h != -1) _HSL_H = h; if (s != -1) @@ -275,72 +275,69 @@ private void RecalculateHSLFromRGB() private void RecalculateHSLFromHSV() { - var hsltuple = HslHelper.HsvToHsl(_HSV_H, _HSV_S, _HSV_V); - double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; - _HSL_H = h; + var hsl = HslHelper.HsvToHsl(_HSV_H, _HSV_S, _HSV_V); + _HSL_H = hsl.H; + + double s = hsl.S; if (s != -1) _HSL_S = s; - _HSL_L = l; + + _HSL_L = HSL_L; } private void RecalculateHSLFromOKHSV() { - var hsltuple = HslHelper.OkHsvToHsl(_OKHSV_H, _OKHSV_S, _OKHSV_V); - double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; - _HSL_H = h; - _HSL_S = s;// todo add -1 checks if necessary - _HSL_L = l; + var hsl = HslHelper.OkHsvToHsl(_OKHSV_H, _OKHSV_S, _OKHSV_V); + _HSL_H = hsl.H; + _HSL_S = hsl.S;// todo add -1 checks if necessary + _HSL_L = hsl.L; } private void RecalculateHSLFromOKHSL() { - var hsltuple = HslHelper.OkHslToHsl(_OKHSL_H, _OKHSL_S, _OKHSL_L); - double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; - _HSL_H = h; - _HSL_S = s;// todo add -1 checks if necessary - _HSL_L = l; + var hsl = HslHelper.OkHslToHsl(_OKHSL_H, _OKHSL_S, _OKHSL_L); + _HSL_H = hsl.H; + _HSL_S = hsl.S;// todo add -1 checks if necessary + _HSL_L = hsl.L; } - - - private void RecalculateHSVFromRGB() { - var hsvtuple = HsvHelper.RgbToHsv(_RGB_R, _RGB_G, _RGB_B); - double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; + var hsv = HsvHelper.RgbToHsv(_RGB_R, _RGB_G, _RGB_B); + double h = hsv.H, s = hsv.S; if (h != -1) _HSV_H = h; if (s != -1) _HSV_S = s; - _HSV_V = v; + _HSV_V = hsv.V; } private void RecalculateHSVFromHSL() { - var hsvtuple = HsvHelper.HslToHsv(_HSL_H, _HSL_S, _HSL_L); - double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; - _HSV_H = h; + var hsv = HsvHelper.HslToHsv(_HSL_H, _HSL_S, _HSL_L); + _HSV_H = hsv.H; + + double s = hsv.S; if (s != -1) _HSV_S = s; - _HSV_V = v; + + _HSV_V = hsv.V; } private void RecalculateHSVFromOKHSV() { - var hsvtuple = HsvHelper.OkHsvToHsv(_OKHSV_H, _OKHSV_S, _OKHSV_V); - double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; - _HSV_H = h; - _HSV_S = s;// todo add -1 checks if necessary - _HSV_V = v; + var hsv = HsvHelper.OkHsvToHsv(_OKHSV_H, _OKHSV_S, _OKHSV_V); + _HSV_H = hsv.H; + _HSV_S = hsv.S;// todo add -1 checks if necessary + _HSV_V = hsv.V; } private void RecalculateHSVFromOKHSL() { - var hsvtuple = HsvHelper.OkHslToHsv(_OKHSL_H, _OKHSL_S, _OKHSL_L); - double h = hsvtuple.Item1, s = hsvtuple.Item2, v = hsvtuple.Item3; - _HSV_H = h; - _HSV_S = s;// todo add -1 checks if necessary - _HSV_V = v; + var hsv = HsvHelper.OkHslToHsv(_OKHSL_H, _OKHSL_S, _OKHSL_L); + _HSV_H = hsv.H; + _HSV_S = hsv.S;// todo add -1 checks if necessary + _HSV_V = hsv.V; } @@ -348,34 +345,34 @@ private void RecalculateHSVFromOKHSL() private void RecalculateRGBFromHSL() { - var rgbtuple = RgbHelper.HslToRgb(_HSL_H, _HSL_S, _HSL_L); - _RGB_R = rgbtuple.Item1; - _RGB_G = rgbtuple.Item2; - _RGB_B = rgbtuple.Item3; + var rgb = RgbHelper.HslToRgb(_HSL_H, _HSL_S, _HSL_L); + _RGB_R = rgb.R; + _RGB_G = rgb.G; + _RGB_B = rgb.B; } private void RecalculateRGBFromHSV() { - var rgbtuple = RgbHelper.HsvToRgb(_HSV_H, _HSV_S, _HSV_V); - _RGB_R = rgbtuple.Item1; - _RGB_G = rgbtuple.Item2; - _RGB_B = rgbtuple.Item3; + var rgb = RgbHelper.HsvToRgb(_HSV_H, _HSV_S, _HSV_V); + _RGB_R = rgb.R; + _RGB_G = rgb.G; + _RGB_B = rgb.B; } private void RecalculateRGBFromOKHSL() { - var rgbtuple = RgbHelper.OkHslToRgb(_OKHSL_H, _OKHSL_S, _OKHSL_L); - _RGB_R = rgbtuple.Item1; - _RGB_G = rgbtuple.Item2;// todo add -1 checks if necessary - _RGB_B = rgbtuple.Item3; + var rgb = RgbHelper.OkHslToRgb(_OKHSL_H, _OKHSL_S, _OKHSL_L); + _RGB_R = rgb.R; + _RGB_G = rgb.G;// todo add -1 checks if necessary + _RGB_B = rgb.B; } private void RecalculateRGBFromOKHSV() { - var rgbtuple = RgbHelper.OkHsvToRgb(_OKHSV_H, _OKHSV_S, _OKHSV_V); - _RGB_R = rgbtuple.Item1; - _RGB_G = rgbtuple.Item2;// todo add -1 checks if necessary - _RGB_B = rgbtuple.Item3; + var rgb = RgbHelper.OkHsvToRgb(_OKHSV_H, _OKHSV_S, _OKHSV_V); + _RGB_R = rgb.R; + _RGB_G = rgb.G;// todo add -1 checks if necessary + _RGB_B = rgb.B; } @@ -383,69 +380,66 @@ private void RecalculateRGBFromOKHSV() private void RecalculateOKHSLFromRGB() { - var okhsltuple = OkHslHelper.RgbToOkHsl(_RGB_R, _RGB_G, _RGB_B); - _OKHSL_H = okhsltuple.Item1; - _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary - _OKHSL_L = okhsltuple.Item3; + var okHsl = OkHslHelper.RgbToOkHsl(_RGB_R, _RGB_G, _RGB_B); + _OKHSL_H = okHsl.H; + _OKHSL_S = okHsl.S;// todo add -1 checks if necessary + _OKHSL_L = okHsl.L; } private void RecalculateOKHSLFromHSL() { - var okhsltuple = OkHslHelper.HslToOkHsl(_HSL_H, _HSL_S, _HSL_L); - _OKHSL_H = okhsltuple.Item1; - _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary - _OKHSL_L = okhsltuple.Item3; + var okHsl = OkHslHelper.HslToOkHsl(_HSL_H, _HSL_S, _HSL_L); + _OKHSL_H = okHsl.H; + _OKHSL_S = okHsl.S;// todo add -1 checks if necessary + _OKHSL_L = okHsl.L; } private void RecalculateOKHSLFromHSV() { - var okhsltuple = OkHslHelper.HsvToOkHsl(_HSV_H, _HSV_S, _HSV_V); - _OKHSL_H = okhsltuple.Item1; - _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary - _OKHSL_L = okhsltuple.Item3; + var okHsl = OkHslHelper.HsvToOkHsl(_HSV_H, _HSV_S, _HSV_V); + _OKHSL_H = okHsl.H; + _OKHSL_S = okHsl.S;// todo add -1 checks if necessary + _OKHSL_L = okHsl.L; } private void RecalculateOKHSLFromOKHSV() { - var okhsltuple = OkHslHelper.OkHsvToOkHsl(_OKHSV_H, _OKHSV_S, _OKHSV_V); - _OKHSL_H = okhsltuple.Item1; - _OKHSL_S = okhsltuple.Item2;// todo add -1 checks if necessary - _OKHSL_L = okhsltuple.Item3; + var okHsl = OkHslHelper.OkHsvToOkHsl(_OKHSV_H, _OKHSV_S, _OKHSV_V); + _OKHSL_H = okHsl.H; + _OKHSL_S = okHsl.S;// todo add -1 checks if necessary + _OKHSL_L = okHsl.L; } - - - private void RecalculateOKHSVFromRGB() { - var okhsvtuple = OkHsvHelper.RgbToOkHsv(_RGB_R, _RGB_G, _RGB_B); - _OKHSV_H = okhsvtuple.Item1; - _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary - _OKHSV_V = okhsvtuple.Item3; + var okHsv = OkHsvHelper.RgbToOkHsv(_RGB_R, _RGB_G, _RGB_B); + _OKHSV_H = okHsv.H; + _OKHSV_S = okHsv.S;// todo add -1 checks if necessary + _OKHSV_V = okHsv.V; } private void RecalculateOKHSVFromHSL() { - var okhsvtuple = OkHsvHelper.HslToOkHsv(_HSL_H, _HSL_S, _HSL_L); - _OKHSV_H = okhsvtuple.Item1; - _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary - _OKHSV_V = okhsvtuple.Item3; + var okHsv = OkHsvHelper.HslToOkHsv(_HSL_H, _HSL_S, _HSL_L); + _OKHSV_H = okHsv.H; + _OKHSV_S = okHsv.S;// todo add -1 checks if necessary + _OKHSV_V = okHsv.V; } private void RecalculateOKHSVFromHSV() { - var okhsvtuple = OkHsvHelper.HsvToOkHsv(_HSV_H, _HSV_S, _HSV_V); - _OKHSV_H = okhsvtuple.Item1; - _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary - _OKHSV_V = okhsvtuple.Item3; + var okHsv = OkHsvHelper.HsvToOkHsv(_HSV_H, _HSV_S, _HSV_V); + _OKHSV_H = okHsv.H; + _OKHSV_S = okHsv.S;// todo add -1 checks if necessary + _OKHSV_V = okHsv.V; } private void RecalculateOKHSVFromOKHSL() { var okhsvtuple = OkHsvHelper.OkHslToOkHsv(_OKHSL_H, _OKHSL_S, _OKHSL_L); - _OKHSV_H = okhsvtuple.Item1; - _OKHSV_S = okhsvtuple.Item2;// todo add -1 checks if necessary - _OKHSV_V = okhsvtuple.Item3; + _OKHSV_H = okhsvtuple.H; + _OKHSV_S = okhsvtuple.S;// todo add -1 checks if necessary + _OKHSV_V = okhsvtuple.V; } } } \ No newline at end of file diff --git a/src/ColorPicker.Models/Colors/Hsl.cs b/src/ColorPicker.Models/Colors/Hsl.cs new file mode 100644 index 0000000..534bcb4 --- /dev/null +++ b/src/ColorPicker.Models/Colors/Hsl.cs @@ -0,0 +1,17 @@ +namespace ColorPicker.Models.Colors; + +public struct Hsl +{ + public double H { get; } + + public double S { get; } + + public double L { get; } + + public Hsl(double h, double s, double l) + { + H = h; + S = s; + L = l; + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/Colors/Hsv.cs b/src/ColorPicker.Models/Colors/Hsv.cs new file mode 100644 index 0000000..545eaad --- /dev/null +++ b/src/ColorPicker.Models/Colors/Hsv.cs @@ -0,0 +1,17 @@ +namespace ColorPicker.Models.Colors; + +public readonly struct Hsv +{ + public double H { get; } + + public double S { get; } + + public double V { get; } + + public Hsv(double h, double s, double v) + { + H = h; + S = s; + V = v; + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/Colors/Lab.cs b/src/ColorPicker.Models/Colors/Lab.cs new file mode 100644 index 0000000..bec2b70 --- /dev/null +++ b/src/ColorPicker.Models/Colors/Lab.cs @@ -0,0 +1,17 @@ +namespace ColorPicker.Models.Colors; + +internal struct Lab +{ + public double L { get; } + + public double a { get; } + + public double b { get; } + + public Lab(double l, double a, double b) + { + L = l; + this.a = a; + this.b = b; + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/Colors/Rgb.cs b/src/ColorPicker.Models/Colors/Rgb.cs new file mode 100644 index 0000000..5d22006 --- /dev/null +++ b/src/ColorPicker.Models/Colors/Rgb.cs @@ -0,0 +1,17 @@ +namespace ColorPicker.Models.Colors; + +public struct Rgb +{ + public double R { get; } + + public double G { get; } + + public double B { get; } + + public Rgb(double r, double g, double b) + { + R = r; + G = g; + B = b; + } +} \ No newline at end of file diff --git a/src/ColorPicker/UserControls/SquareSlider.xaml.cs b/src/ColorPicker/UserControls/SquareSlider.xaml.cs index c4093b3..8a611d3 100644 --- a/src/ColorPicker/UserControls/SquareSlider.xaml.cs +++ b/src/ColorPicker/UserControls/SquareSlider.xaml.cs @@ -34,13 +34,14 @@ public static readonly DependencyProperty PickerTypeProperty private double _rangeX; private double _rangeY; - private Func> colorSpaceConversionMethod = RgbHelper.HsvToRgb; - + private Action recalculateGradientMethod; + public SquareSlider() { GradientBitmap = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Rgb24, null); InitializeComponent(); - RecalculateGradient(); + recalculateGradientMethod = RecalculateGradientHsv; + recalculateGradientMethod(); } public double Hue @@ -99,7 +100,64 @@ public WriteableBitmap GradientBitmap public event PropertyChangedEventHandler PropertyChanged; - private void RecalculateGradient() + private void RecalculateGradientHsv() + { + var w = GradientBitmap.PixelWidth; + var h = GradientBitmap.PixelHeight; + var hue = Hue; + var pixels = new byte[w * h * 3]; + for (var j = 0; j < h; j++) + for (var i = 0; i < w; i++) + { + var rgb = RgbHelper.HslToRgb(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); + var pos = (j * h + i) * 3; + pixels[pos] = (byte)(rgb.R * 255); + pixels[pos + 1] = (byte)(rgb.G * 255); + pixels[pos + 2] = (byte)(rgb.B * 255); + } + + GradientBitmap.WritePixels(new Int32Rect(0, 0, w, h), pixels, w * 3, 0); + } + + private void RecalculateGradientHsl() + { + var w = GradientBitmap.PixelWidth; + var h = GradientBitmap.PixelHeight; + var hue = Hue; + var pixels = new byte[w * h * 3]; + for (var j = 0; j < h; j++) + for (var i = 0; i < w; i++) + { + var rgb = RgbHelper.HslToRgb(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); + var pos = (j * h + i) * 3; + pixels[pos] = (byte)(rgb.R * 255); + pixels[pos + 1] = (byte)(rgb.G * 255); + pixels[pos + 2] = (byte)(rgb.B * 255); + } + + GradientBitmap.WritePixels(new Int32Rect(0, 0, w, h), pixels, w * 3, 0); + } + + private void RecalculateGradientOkHsv() + { + var w = GradientBitmap.PixelWidth; + var h = GradientBitmap.PixelHeight; + var hue = Hue; + var pixels = new byte[w * h * 3]; + for (var j = 0; j < h; j++) + for (var i = 0; i < w; i++) + { + var rgb = RgbHelper.OkHsvToRgb(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); + var pos = (j * h + i) * 3; + pixels[pos] = (byte)(rgb.R * 255); + pixels[pos + 1] = (byte)(rgb.G * 255); + pixels[pos + 2] = (byte)(rgb.B * 255); + } + + GradientBitmap.WritePixels(new Int32Rect(0, 0, w, h), pixels, w * 3, 0); + } + + private void RecalculateGradientOkHsl() { var w = GradientBitmap.PixelWidth; var h = GradientBitmap.PixelHeight; @@ -108,12 +166,11 @@ private void RecalculateGradient() for (var j = 0; j < h; j++) for (var i = 0; i < w; i++) { - var rgbtuple = colorSpaceConversionMethod(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); - double r = rgbtuple.Item1, g = rgbtuple.Item2, b = rgbtuple.Item3; + var rgb = RgbHelper.OkHslToRgb(hue, i / (double)(w - 1), (h - 1 - j) / (double)(h - 1)); var pos = (j * h + i) * 3; - pixels[pos] = (byte)(r * 255); - pixels[pos + 1] = (byte)(g * 255); - pixels[pos + 2] = (byte)(b * 255); + pixels[pos] = (byte)(rgb.R * 255); + pixels[pos + 1] = (byte)(rgb.G * 255); + pixels[pos + 2] = (byte)(rgb.B * 255); } GradientBitmap.WritePixels(new Int32Rect(0, 0, w, h), pixels, w * 3, 0); @@ -125,28 +182,28 @@ private static void OnColorSpaceChanged(DependencyObject d, DependencyPropertyCh switch ((PickerType)args.NewValue) { case PickerType.HSV: - sender.colorSpaceConversionMethod = RgbHelper.HsvToRgb; + sender.recalculateGradientMethod = sender.RecalculateGradientHsv; break; case PickerType.HSL: - sender.colorSpaceConversionMethod = RgbHelper.HslToRgb; + sender.recalculateGradientMethod = sender.RecalculateGradientHsl; break; case PickerType.OKHSV: - sender.colorSpaceConversionMethod = RgbHelper.OkHsvToRgb; + sender.recalculateGradientMethod = sender.RecalculateGradientOkHsv; break; case PickerType.OKHSL: - sender.colorSpaceConversionMethod = RgbHelper.OkHslToRgb; + sender.recalculateGradientMethod = sender.RecalculateGradientOkHsl; break; default: - sender.colorSpaceConversionMethod = RgbHelper.HslToRgb; + sender.recalculateGradientMethod = sender.RecalculateGradientHsl; break; } - sender.RecalculateGradient(); + sender.recalculateGradientMethod(); } private static void OnHueChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) { - ((SquareSlider)d).RecalculateGradient(); + ((SquareSlider)d).recalculateGradientMethod(); } private void OnMouseDown(object sender, MouseButtonEventArgs e)