/ src / modules / cmdpal / Microsoft.CmdPal.UI / Helpers / ColorExtensions.cs
ColorExtensions.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using CommunityToolkit.WinUI.Helpers;
  6  using Windows.UI;
  7  
  8  namespace Microsoft.CmdPal.UI.Helpers;
  9  
 10  /// <summary>
 11  /// Extension methods for <see cref="Color"/>.
 12  /// </summary>
 13  internal static class ColorExtensions
 14  {
 15      /// <param name="color">Input color.</param>
 16      public static double CalculateBrightness(this Color color)
 17      {
 18          return color.ToHsv().V;
 19      }
 20  
 21      /// <summary>
 22      /// Allows to change the brightness by a factor based on the HSV color space.
 23      /// </summary>
 24      /// <param name="color">Input color.</param>
 25      /// <param name="brightnessFactor">The brightness adjustment factor, ranging from -1 to 1.</param>
 26      /// <returns>Updated color.</returns>
 27      public static Color UpdateBrightness(this Color color, double brightnessFactor)
 28      {
 29          ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1);
 30          ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1);
 31  
 32          var hsvColor = color.ToHsv();
 33          return ColorHelper.FromHsv(hsvColor.H, hsvColor.S, Math.Clamp(hsvColor.V + brightnessFactor, 0, 1), hsvColor.A);
 34      }
 35  
 36      /// <summary>
 37      /// Updates the color by adjusting brightness, saturation, and luminance factors.
 38      /// </summary>
 39      /// <param name="color">Input color.</param>
 40      /// <param name="brightnessFactor">The brightness adjustment factor, ranging from -1 to 1.</param>
 41      /// <param name="saturationFactor">The saturation adjustment factor, ranging from -1 to 1. Defaults to 0.</param>
 42      /// <param name="luminanceFactor">The luminance adjustment factor, ranging from -1 to 1. Defaults to 0.</param>
 43      /// <returns>Updated color.</returns>
 44      public static Color Update(this Color color, double brightnessFactor, double saturationFactor = 0, double luminanceFactor = 0)
 45      {
 46          ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1);
 47          ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1);
 48  
 49          ArgumentOutOfRangeException.ThrowIfGreaterThan(saturationFactor, 1);
 50          ArgumentOutOfRangeException.ThrowIfLessThan(saturationFactor, -1);
 51  
 52          ArgumentOutOfRangeException.ThrowIfGreaterThan(luminanceFactor, 1);
 53          ArgumentOutOfRangeException.ThrowIfLessThan(luminanceFactor, -1);
 54  
 55          var hsv = color.ToHsv();
 56  
 57          var rgb = ColorHelper.FromHsv(
 58              hsv.H,
 59              Clamp01(hsv.S + saturationFactor),
 60              Clamp01(hsv.V + brightnessFactor));
 61  
 62          if (luminanceFactor == 0)
 63          {
 64              return rgb;
 65          }
 66  
 67          var hsl = rgb.ToHsl();
 68          var lightness = Clamp01(hsl.L + luminanceFactor);
 69          return ColorHelper.FromHsl(hsl.H, hsl.S, lightness);
 70      }
 71  
 72      /// <summary>
 73      /// Linearly interpolates between two colors in HSV space.
 74      /// Hue is blended along the shortest arc on the color wheel (wrap-aware).
 75      /// Saturation, Value, and Alpha are blended linearly.
 76      /// </summary>
 77      /// <param name="a">Start color.</param>
 78      /// <param name="b">End color.</param>
 79      /// <param name="t">Interpolation factor in [0,1].</param>
 80      /// <returns>Interpolated color.</returns>
 81      public static Color LerpHsv(this Color a, Color b, double t)
 82      {
 83          t = Clamp01(t);
 84  
 85          // Convert to HSV
 86          var hslA = a.ToHsv();
 87          var hslB = b.ToHsv();
 88  
 89          var h1 = hslA.H;
 90          var h2 = hslB.H;
 91  
 92          // Handle near-gray hues (undefined hue) by inheriting the other's hue
 93          const double satEps = 1e-4f;
 94          if (hslA.S < satEps && hslB.S >= satEps)
 95          {
 96              h1 = h2;
 97          }
 98          else if (hslB.S < satEps && hslA.S >= satEps)
 99          {
100              h2 = h1;
101          }
102  
103          return ColorHelper.FromHsv(
104              hue: LerpHueDegrees(h1, h2, t),
105              saturation: Lerp(hslA.S, hslB.S, t),
106              value: Lerp(hslA.V, hslB.V, t),
107              alpha: (byte)Math.Round(Lerp(hslA.A, hslB.A, t)));
108      }
109  
110      private static double LerpHueDegrees(double a, double b, double t)
111      {
112          a = Mod360(a);
113          b = Mod360(b);
114          var delta = ((b - a + 540f) % 360f) - 180f;
115          return Mod360(a + (delta * t));
116      }
117  
118      private static double Mod360(double angle)
119      {
120          angle %= 360f;
121          if (angle < 0f)
122          {
123              angle += 360f;
124          }
125  
126          return angle;
127      }
128  
129      private static double Lerp(double a, double b, double t) => a + ((b - a) * t);
130  
131      private static double Clamp01(double x) => Math.Clamp(x, 0, 1);
132  }