Gradient.cs 8.1 KB

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