Gradient.cs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. /// <summary>
  34. /// The discrete colors that will make up the <see cref="Gradient"/>.
  35. /// </summary>
  36. public List<Color> Spectrum { get; private set; }
  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">The number of colors to generate between each pair (must be N-1 numbers).
  47. /// If only one step is passed then it is assumed to be the same distance for all pairs.</param>
  48. /// <param name="loop">True to duplicate the first stop and step so that the gradient repeats itself</param>
  49. /// <exception cref="ArgumentException"></exception>
  50. public Gradient (IEnumerable<Color> stops, IEnumerable<int> steps, bool loop = false)
  51. {
  52. _stops = stops.ToList ();
  53. if (_stops.Count < 1)
  54. {
  55. throw new ArgumentException ("At least one color stop must be provided.");
  56. }
  57. _steps = steps.ToList ();
  58. // If multiple colors and only 1 step assume same distance applies to all steps
  59. if (_stops.Count > 2 && _steps.Count == 1)
  60. {
  61. _steps = Enumerable.Repeat (_steps.Single (),_stops.Count() - 1).ToList();
  62. }
  63. if (_steps.Any (step => step < 1))
  64. {
  65. throw new ArgumentException ("Steps must be greater than 0.");
  66. }
  67. if (_steps.Count != _stops.Count - 1)
  68. {
  69. throw new ArgumentException ("Number of steps must be N-1");
  70. }
  71. _loop = loop;
  72. Spectrum = GenerateGradient (_steps);
  73. }
  74. /// <summary>
  75. /// Returns the color to use at the given part of the spectrum
  76. /// </summary>
  77. /// <param name="fraction">Proportion of the way through the spectrum, must be between
  78. /// 0 and 1 (inclusive). Returns the last color if <paramref name="fraction"/> is
  79. /// <see cref="double.NaN"/>.</param>
  80. /// <returns></returns>
  81. /// <exception cref="ArgumentOutOfRangeException"></exception>
  82. public Color GetColorAtFraction (double fraction)
  83. {
  84. if (double.IsNaN (fraction))
  85. {
  86. return Spectrum.Last ();
  87. }
  88. if (fraction < 0 || fraction > 1)
  89. {
  90. throw new ArgumentOutOfRangeException (nameof (fraction), "Fraction must be between 0 and 1.");
  91. }
  92. int index = (int)(fraction * (Spectrum.Count - 1));
  93. return Spectrum [index];
  94. }
  95. private List<Color> GenerateGradient (IEnumerable<int> steps)
  96. {
  97. List<Color> gradient = new List<Color> ();
  98. if (_stops.Count == 1)
  99. {
  100. for (int i = 0; i < steps.Sum (); i++)
  101. {
  102. gradient.Add (_stops [0]);
  103. }
  104. return gradient;
  105. }
  106. var stopsToUse = _stops.ToList ();
  107. var stepsToUse = _steps.ToList ();
  108. if (_loop)
  109. {
  110. stopsToUse.Add (_stops [0]);
  111. stepsToUse.Add (_steps.First ());
  112. }
  113. var colorPairs = stopsToUse.Zip (stopsToUse.Skip (1), (start, end) => new { start, end });
  114. var stepsList = stepsToUse;
  115. foreach (var (colorPair, thesteps) in colorPairs.Zip (stepsList, (pair, step) => (pair, step)))
  116. {
  117. gradient.AddRange (InterpolateColors (colorPair.start, colorPair.end, thesteps));
  118. }
  119. return gradient;
  120. }
  121. private IEnumerable<Color> InterpolateColors (Color start, Color end, int steps)
  122. {
  123. for (int step = 0; step < steps; step++)
  124. {
  125. double fraction = (double)step / steps;
  126. int r = (int)(start.R + fraction * (end.R - start.R));
  127. int g = (int)(start.G + fraction * (end.G - start.G));
  128. int b = (int)(start.B + fraction * (end.B - start.B));
  129. yield return new Color (r, g, b);
  130. }
  131. yield return end; // Ensure the last color is included
  132. }
  133. /// <summary>
  134. /// <para>
  135. /// Creates a mapping starting at 0,0 and going to <paramref name="maxRow"/> and <paramref name="maxColumn"/>
  136. /// (inclusively) using the supplied <paramref name="direction"/>.
  137. /// </para>
  138. /// <para>
  139. /// Note that this method is inclusive i.e. passing 1/1 results in 4 mapped coordinates.
  140. /// </para>
  141. /// </summary>
  142. /// <param name="maxRow"></param>
  143. /// <param name="maxColumn"></param>
  144. /// <param name="direction"></param>
  145. /// <returns></returns>
  146. public Dictionary<Point, Color> BuildCoordinateColorMapping (int maxRow, int maxColumn, GradientDirection direction)
  147. {
  148. var gradientMapping = new Dictionary<Point, Color> ();
  149. switch (direction)
  150. {
  151. case GradientDirection.Vertical:
  152. for (int row = 0; row <= maxRow; row++)
  153. {
  154. double fraction = maxRow == 0 ? 1.0 : (double)row / maxRow;
  155. Color color = GetColorAtFraction (fraction);
  156. for (int col = 0; col <= maxColumn; col++)
  157. {
  158. gradientMapping [new Point (col, row)] = color;
  159. }
  160. }
  161. break;
  162. case GradientDirection.Horizontal:
  163. for (int col = 0; col <= maxColumn; col++)
  164. {
  165. double fraction = maxColumn == 0 ? 1.0 : (double)col / maxColumn;
  166. Color color = GetColorAtFraction (fraction);
  167. for (int row = 0; row <= maxRow; row++)
  168. {
  169. gradientMapping [new Point (col, row)] = color;
  170. }
  171. }
  172. break;
  173. case GradientDirection.Radial:
  174. for (int row = 0; row <= maxRow; row++)
  175. {
  176. for (int col = 0; col <= maxColumn; col++)
  177. {
  178. double distanceFromCenter = FindNormalizedDistanceFromCenter (maxRow, maxColumn, new Point (col, row));
  179. Color color = GetColorAtFraction (distanceFromCenter);
  180. gradientMapping [new Point (col, row)] = color;
  181. }
  182. }
  183. break;
  184. case GradientDirection.Diagonal:
  185. for (int row = 0; row <= maxRow; row++)
  186. {
  187. for (int col = 0; col <= maxColumn; col++)
  188. {
  189. double fraction = ((double)row * 2 + col) / (maxRow * 2 + maxColumn);
  190. Color color = GetColorAtFraction (fraction);
  191. gradientMapping [new Point (col, row)] = color;
  192. }
  193. }
  194. break;
  195. }
  196. return gradientMapping;
  197. }
  198. private double FindNormalizedDistanceFromCenter (int maxRow, int maxColumn, Point coord)
  199. {
  200. double centerX = maxColumn / 2.0;
  201. double centerY = maxRow / 2.0;
  202. double dx = coord.X - centerX;
  203. double dy = coord.Y - centerY;
  204. double distance = Math.Sqrt (dx * dx + dy * dy);
  205. double maxDistance = Math.Sqrt (centerX * centerX + centerY * centerY);
  206. return distance / maxDistance;
  207. }
  208. }