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 }