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 5c03cc9..6370918 100644 --- a/src/ColorPicker.Models/ColorPicker.Models.csproj +++ b/src/ColorPicker.Models/ColorPicker.Models.csproj @@ -30,4 +30,8 @@ + + + + diff --git a/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs b/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs new file mode 100644 index 0000000..78c17a1 --- /dev/null +++ b/src/ColorPicker.Models/ColorSliders/ColorSliderGradientPoint.cs @@ -0,0 +1,29 @@ +using System; +using ColorPicker.Models.Colors; + +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(Rgb rgb, double position) + { + R = rgb.R; + G = rgb.G; + B = rgb.B; + 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..aabb2d6 --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/HslHelper.cs @@ -0,0 +1,96 @@ +using System; +using ColorPicker.Models.Colors; + +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 Hsl 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 Hsl(-1, -1, 0); + + if (delta == 0) + //gray + return new Hsl(-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 Hsl(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 Hsl 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 Hsl(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 Hsl OkHslToHsl(double h, double s, double l) + { + var rgb = RgbHelper.OkHslToRgb(h, s, l); + return HslHelper.RgbToHsl(rgb.R, rgb.G, rgb.B); + } + + /// + /// 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 Hsl OkHsvToHsl(double h, double s, double v) + { + var rgb = RgbHelper.OkHsvToRgb(h, s, v); + 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 new file mode 100644 index 0000000..4b21dbf --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/HsvHelper.cs @@ -0,0 +1,94 @@ +using System; +using ColorPicker.Models.Colors; + +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 Hsv 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 Hsv(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 Hsv 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 Hsv(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 Hsv(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 Hsv OkHslToHsv(double h, double s, double l) + { + var rgb = RgbHelper.OkHslToRgb(h, s, l); + return HsvHelper.RgbToHsv(rgb.R, rgb.G, rgb.B); + } + + /// + /// 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 Hsv OkHsvToHsv(double h, double s, double v) + { + var rgb = RgbHelper.OkHsvToRgb(h, s, v); + 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 new file mode 100644 index 0000000..b4c476a --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/OkHelper.cs @@ -0,0 +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; + +public static class OkHelper +{ + 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(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 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) + { + // 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(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; + + 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 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); + } + + 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, C * a_, C * b_)); + + return new Rgb( + SrgbTransferFunction(rgb.R), + SrgbTransferFunction(rgb.G), + SrgbTransferFunction(rgb.B) + ); + } + + public static Hsl SrgbToOkHsl(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 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; + } + + return new Hsl(h, s, Toe(L)); + } + + + 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); + + 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_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 Rgb( + SrgbTransferFunction(rgb.R), + SrgbTransferFunction(rgb.G), + SrgbTransferFunction(rgb.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 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_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; + + 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 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 new file mode 100644 index 0000000..5d301e4 --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/OkHslHelper.cs @@ -0,0 +1,59 @@ +using System; +using ColorPicker.Models.Colors; + +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 Hsl RgbToOkHsl(double r, double g, double b) + { + var hsl = OkHelper.SrgbToOkHsl(r, g, b); + return new Hsl(hsl.H * 360, hsl.S, hsl.L); + } + + /// + /// 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 Hsl HslToOkHsl(double h, double s, double l) + { + var rgb = RgbHelper.HslToRgb(h, s, l); + return OkHslHelper.RgbToOkHsl(rgb.R, rgb.G, rgb.B); + } + + /// + /// 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 Hsl HsvToOkHsl(double h, double s, double v) + { + var rgb = RgbHelper.HsvToRgb(h, s, v); + return OkHslHelper.RgbToOkHsl(rgb.R, rgb.G, rgb.B); + } + + /// + /// 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 Hsl OkHsvToOkHsl(double h, double s, double v) + { + var rgb = RgbHelper.OkHsvToRgb(h, s, v); + 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 new file mode 100644 index 0000000..7272de3 --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/OkHsvHelper.cs @@ -0,0 +1,59 @@ +using System; +using ColorPicker.Models.Colors; + +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 Hsv RgbToOkHsv(double r, double g, double b) + { + var hsl = OkHelper.SrgbToOkHsl(r, g, b); + return new Hsv(hsl.H * 360, hsl.S, hsl.L); + } + + /// + /// 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 Hsv HslToOkHsv(double h, double s, double l) + { + var rgb = RgbHelper.HslToRgb(h, s, l); + return OkHsvHelper.RgbToOkHsv(rgb.R, rgb.G, rgb.B); + } + + /// + /// 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 Hsv HsvToOkHsv(double h, double s, double v) + { + var rgb = RgbHelper.HsvToRgb(h, s, v); + return OkHsvHelper.RgbToOkHsv(rgb.R, rgb.G, rgb.B); + } + + /// + /// 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 Hsv OkHslToOkHsv(double h, double s, double l) + { + var rgb = RgbHelper.OkHslToRgb(h, s, l); + 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 new file mode 100644 index 0000000..2ebe90c --- /dev/null +++ b/src/ColorPicker.Models/ColorSpaces/RgbHelper.cs @@ -0,0 +1,104 @@ +using System; +using ColorPicker.Models.Colors; + +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 Rgb HsvToRgb(double h, double s, double v) + { + if (s == 0) + // achromatic (grey) + return new Rgb(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 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); + } + } + + /// + /// Converts HSL to RGB + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values (0-1) in order: R, G, B + public static Rgb 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 Rgb(maxRGB, delta * circleSegmentFraction + minRGB, + minRGB); //red-yellow + case 1: + return new Rgb(delta * (1 - circleSegmentFraction) + minRGB, maxRGB, + minRGB); //yellow-green + case 2: + return new Rgb(minRGB, maxRGB, + delta * circleSegmentFraction + minRGB); //green-cyan + case 3: + return new Rgb(minRGB, delta * (1 - circleSegmentFraction) + minRGB, + maxRGB); //cyan-blue + case 4: + return new Rgb(delta * circleSegmentFraction + minRGB, minRGB, + maxRGB); //blue-purple + default: + return new Rgb(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 Rgb OkHsvToRgb(double h, double s, double v) + { + var rgb = OkHelper.OkHsvToSrgb(h / 360, s, v); + return new Rgb(rgb.R, rgb.G, rgb.B); + } + + /// + /// Converts OKHSL to RGB + /// + /// Hue, 0-360 + /// Saturation, 0-1 + /// Lightness, 0-1 + /// Values (0-1) in order: R, G, B + public static Rgb OkHslToRgb(double h, double s, double l) + { + 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 77ed4fe..12ad551 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,13 +176,96 @@ 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); - double h = hsltuple.Item1, s = hsltuple.Item2, l = hsltuple.Item3; + var hsltuple = HslHelper.RgbToHsl(_RGB_R, _RGB_G, _RGB_B); + double h = hsltuple.H, s = hsltuple.S, l = hsltuple.L; if (h != -1) _HSL_H = h; if (s != -1) @@ -153,49 +275,171 @@ private void RecalculateHSLFromRGB() private void RecalculateHSLFromHSV() { - var hsltuple = ColorSpaceHelper.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 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 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 = ColorSpaceHelper.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 = ColorSpaceHelper.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 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 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; + } + + + private void RecalculateRGBFromHSL() { - var rgbtuple = ColorSpaceHelper.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 = ColorSpaceHelper.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 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 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; + } + + + + + private void RecalculateOKHSLFromRGB() + { + 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 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 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 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 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 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 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.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.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 0000000..d477ad5 Binary files /dev/null and b/src/ColorPicker/Images/CircularHueGradientOkhsl.png differ diff --git a/src/ColorPicker/Images/CircularHueGradientOkhsv.png b/src/ColorPicker/Images/CircularHueGradientOkhsv.png new file mode 100644 index 0000000..e3202bc Binary files /dev/null and b/src/ColorPicker/Images/CircularHueGradientOkhsv.png differ diff --git a/src/ColorPicker/PickerControlBase.cs b/src/ColorPicker/PickerControlBase.cs index 9b2c5de..0174452 100644 --- a/src/ColorPicker/PickerControlBase.cs +++ b/src/ColorPicker/PickerControlBase.cs @@ -10,7 +10,7 @@ public class PickerControlBase : UserControl, IColorStateStorage { public static readonly DependencyProperty ColorStateProperty = DependencyProperty.Register(nameof(ColorState), typeof(ColorState), typeof(PickerControlBase), - new PropertyMetadata(new ColorState(0, 0, 0, 1, 0, 0, 0, 0, 0, 0), OnColorStatePropertyChange)); + new PropertyMetadata(new ColorState(0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), OnColorStatePropertyChange)); public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(PickerControlBase), diff --git a/src/ColorPicker/SquarePicker.xaml b/src/ColorPicker/SquarePicker.xaml index f251236..4ad15c7 100644 --- a/src/ColorPicker/SquarePicker.xaml +++ b/src/ColorPicker/SquarePicker.xaml @@ -41,20 +41,47 @@ + 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..8a611d3 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,16 +34,16 @@ public static readonly DependencyProperty PickerTypeProperty private double _rangeX; private double _rangeY; - private Func> colorSpaceConversionMethod = - ColorSpaceHelper.HsvToRgb; - + private Action recalculateGradientMethod; + public SquareSlider() { GradientBitmap = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Rgb24, null); InitializeComponent(); - RecalculateGradient(); + recalculateGradientMethod = RecalculateGradientHsv; + recalculateGradientMethod(); } - + public double Hue { get => (double)GetValue(HueProperty); @@ -98,7 +100,45 @@ 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; @@ -107,12 +147,30 @@ 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.OkHsvToRgb(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); + } + + private void RecalculateGradientOkHsl() + { + 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.OkHslToRgb(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); @@ -121,17 +179,31 @@ 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.recalculateGradientMethod = sender.RecalculateGradientHsv; + break; + case PickerType.HSL: + sender.recalculateGradientMethod = sender.RecalculateGradientHsl; + break; + case PickerType.OKHSV: + sender.recalculateGradientMethod = sender.RecalculateGradientOkHsv; + break; + case PickerType.OKHSL: + sender.recalculateGradientMethod = sender.RecalculateGradientOkHsl; + break; + default: + 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)