Gradient.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. // This code is a C# port from python library Terminal Text Effects https://github.com/ChrisBuilds/terminaltexteffects/
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Describes the pattern that a <see cref="Gradient"/> results in e.g. <see cref="Vertical"/>,
  5. /// <see cref="Horizontal"/> etc
  6. /// </summary>
  7. public enum GradientDirection
  8. {
  9. /// <summary>
  10. /// Color varies along Y axis but is constant on X axis.
  11. /// </summary>
  12. Vertical,
  13. /// <summary>
  14. /// Color varies along X axis but is constant on Y axis.
  15. /// </summary>
  16. Horizontal,
  17. /// <summary>
  18. /// Color varies by distance from center (i.e. in circular ripples)
  19. /// </summary>
  20. Radial,
  21. /// <summary>
  22. /// Color varies by X and Y axis (i.e. a slanted gradient)
  23. /// </summary>
  24. Diagonal
  25. }
  26. /// <summary>
  27. /// Describes a <see cref="Spectrum"/> of colors that can be combined
  28. /// to make a color gradient. Use <see cref="BuildCoordinateColorMapping"/>
  29. /// to create into gradient fill area maps.
  30. /// </summary>
  31. public class Gradient
  32. {
  33. /// <summary>
  34. /// The discrete colors that will make up the <see cref="Gradient"/>.
  35. /// </summary>
  36. public List<Color> Spectrum { get; }
  37. private readonly bool _loop;
  38. private readonly List<Color> _stops;
  39. private readonly List<int> _steps;
  40. /// <summary>
  41. /// Creates a new instance of the <see cref="Gradient"/> class which hosts a <see cref="Spectrum"/>
  42. /// of colors including all <paramref name="stops"/> and <paramref name="steps"/> interpolated colors
  43. /// between each corresponding pair.
  44. /// </summary>
  45. /// <param name="stops">The colors to use in the spectrum (N)</param>
  46. /// <param name="steps">
  47. /// The number of colors to generate between each pair (must be N-1 numbers).
  48. /// If only one step is passed then it is assumed to be the same distance for all pairs.
  49. /// </param>
  50. /// <param name="loop">True to duplicate the first stop and step so that the gradient repeats itself</param>
  51. /// <exception cref="ArgumentException"></exception>
  52. public Gradient (IEnumerable<Color> stops, IEnumerable<int> steps, bool loop = false)
  53. {
  54. _stops = stops.ToList ();
  55. if (_stops.Count < 1)
  56. {
  57. throw new ArgumentException ("At least one color stop must be provided.");
  58. }
  59. _steps = steps.ToList ();
  60. // If multiple colors and only 1 step assume same distance applies to all steps
  61. if (_stops.Count > 2 && _steps.Count == 1)
  62. {
  63. _steps = Enumerable.Repeat (_steps.Single (), _stops.Count () - 1).ToList ();
  64. }
  65. if (_steps.Any (step => step < 1))
  66. {
  67. throw new ArgumentException ("Steps must be greater than 0.");
  68. }
  69. if (_steps.Count != _stops.Count - 1)
  70. {
  71. throw new ArgumentException ("Number of steps must be N-1");
  72. }
  73. _loop = loop;
  74. Spectrum = GenerateGradient (_steps);
  75. }
  76. /// <summary>
  77. /// Returns the color to use at the given part of the spectrum
  78. /// </summary>
  79. /// <param name="fraction">
  80. /// Proportion of the way through the spectrum, must be between
  81. /// 0 and 1 (inclusive). Returns the last color if <paramref name="fraction"/> is
  82. /// <see cref="double.NaN"/>.
  83. /// </param>
  84. /// <returns></returns>
  85. /// <exception cref="ArgumentOutOfRangeException"></exception>
  86. public Color GetColorAtFraction (double fraction)
  87. {
  88. if (double.IsNaN (fraction))
  89. {
  90. return Spectrum.Last ();
  91. }
  92. if (fraction is < 0 or > 1)
  93. {
  94. throw new ArgumentOutOfRangeException (nameof (fraction), @"Fraction must be between 0 and 1.");
  95. }
  96. var index = (int)(fraction * (Spectrum.Count - 1));
  97. return Spectrum [index];
  98. }
  99. private List<Color> GenerateGradient (IEnumerable<int> steps)
  100. {
  101. List<Color> gradient = new ();
  102. if (_stops.Count == 1)
  103. {
  104. for (var i = 0; i < steps.Sum (); i++)
  105. {
  106. gradient.Add (_stops [0]);
  107. }
  108. return gradient;
  109. }
  110. List<Color> stopsToUse = _stops.ToList ();
  111. List<int> stepsToUse = _steps.ToList ();
  112. if (_loop)
  113. {
  114. stopsToUse.Add (_stops [0]);
  115. stepsToUse.Add (_steps.First ());
  116. }
  117. var colorPairs = stopsToUse.Zip (stopsToUse.Skip (1), (start, end) => new { start, end });
  118. List<int> stepsList = stepsToUse;
  119. foreach ((var colorPair, int thesteps) in colorPairs.Zip (stepsList, (pair, step) => (pair, step)))
  120. {
  121. gradient.AddRange (InterpolateColors (colorPair.start, colorPair.end, thesteps));
  122. }
  123. return gradient;
  124. }
  125. private static IEnumerable<Color> InterpolateColors (Color start, Color end, int steps)
  126. {
  127. for (var step = 0; step < steps; step++)
  128. {
  129. double fraction = (double)step / steps;
  130. var r = (int)(start.R + fraction * (end.R - start.R));
  131. var g = (int)(start.G + fraction * (end.G - start.G));
  132. var b = (int)(start.B + fraction * (end.B - start.B));
  133. yield return new (r, g, b);
  134. }
  135. yield return end; // Ensure the last color is included
  136. }
  137. /// <summary>
  138. /// <para>
  139. /// Creates a mapping starting at 0,0 and going to <paramref name="maxRow"/> and <paramref name="maxColumn"/>
  140. /// (inclusively) using the supplied <paramref name="direction"/>.
  141. /// </para>
  142. /// <para>
  143. /// Note that this method is inclusive i.e. passing 1/1 results in 4 mapped coordinates.
  144. /// </para>
  145. /// </summary>
  146. /// <param name="maxRow"></param>
  147. /// <param name="maxColumn"></param>
  148. /// <param name="direction"></param>
  149. /// <returns></returns>
  150. public Dictionary<Point, Color> BuildCoordinateColorMapping (int maxRow, int maxColumn, GradientDirection direction)
  151. {
  152. Dictionary<Point, Color> gradientMapping = new ();
  153. switch (direction)
  154. {
  155. case GradientDirection.Vertical:
  156. for (var row = 0; row <= maxRow; row++)
  157. {
  158. double fraction = maxRow == 0 ? 1.0 : (double)row / maxRow;
  159. Color color = GetColorAtFraction (fraction);
  160. for (var col = 0; col <= maxColumn; col++)
  161. {
  162. gradientMapping [new (col, row)] = color;
  163. }
  164. }
  165. break;
  166. case GradientDirection.Horizontal:
  167. for (var col = 0; col <= maxColumn; col++)
  168. {
  169. double fraction = maxColumn == 0 ? 1.0 : (double)col / maxColumn;
  170. Color color = GetColorAtFraction (fraction);
  171. for (var row = 0; row <= maxRow; row++)
  172. {
  173. gradientMapping [new (col, row)] = color;
  174. }
  175. }
  176. break;
  177. case GradientDirection.Radial:
  178. for (var row = 0; row <= maxRow; row++)
  179. {
  180. for (var col = 0; col <= maxColumn; col++)
  181. {
  182. double distanceFromCenter = FindNormalizedDistanceFromCenter (maxRow, maxColumn, new (col, row));
  183. Color color = GetColorAtFraction (distanceFromCenter);
  184. gradientMapping [new (col, row)] = color;
  185. }
  186. }
  187. break;
  188. case GradientDirection.Diagonal:
  189. for (var row = 0; row <= maxRow; row++)
  190. {
  191. for (var col = 0; col <= maxColumn; col++)
  192. {
  193. double fraction = ((double)row * 2 + col) / (maxRow * 2 + maxColumn);
  194. Color color = GetColorAtFraction (fraction);
  195. gradientMapping [new (col, row)] = color;
  196. }
  197. }
  198. break;
  199. }
  200. return gradientMapping;
  201. }
  202. private static double FindNormalizedDistanceFromCenter (int maxRow, int maxColumn, Point coord)
  203. {
  204. double centerX = maxColumn / 2.0;
  205. double centerY = maxRow / 2.0;
  206. double dx = coord.X - centerX;
  207. double dy = coord.Y - centerY;
  208. double distance = Math.Sqrt (dx * dx + dy * dy);
  209. double maxDistance = Math.Sqrt (centerX * centerX + centerY * centerY);
  210. return distance / maxDistance;
  211. }
  212. }