Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradients - From Terminal Text Effects #3586

Merged
merged 40 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f312022
Start pulling Animation a bit at a time through chat gpt from https:/…
tznind Jul 5, 2024
7d62ad2
More random code ported
tznind Jul 5, 2024
8fbf4d5
Building
tznind Jul 6, 2024
b82eda4
Rainbow gradient!
tznind Jul 6, 2024
5cac659
Investigate adding a bouncing ball animation
tznind Jul 6, 2024
d7a4e0e
Ball bouncing properly
tznind Jul 6, 2024
ab07f53
Radial gradient
tznind Jul 6, 2024
9672de7
Add other gradients
tznind Jul 6, 2024
1f13ec5
Streamline gradients example and add tabs
tznind Jul 6, 2024
3171df8
LineCanvas support for gradient fill
tznind Jul 7, 2024
c1e82e6
Add tests, corners not working properly for some reason
tznind Jul 7, 2024
a5c1d73
Fix bug in StraightLine as it calculates intersections
tznind Jul 7, 2024
e80f61b
Restore diagonal demo
tznind Jul 7, 2024
cbcf4b5
Remove everything except gradient
tznind Jul 7, 2024
f7d584b
Add attribution
tznind Jul 7, 2024
5a28eb9
Merge branch 'v2_develop' into gradients
tznind Jul 7, 2024
be764b7
xml doc
tznind Jul 7, 2024
a167366
Tests, xmldoc and guards
tznind Jul 7, 2024
116cba8
Gradient tests
tznind Jul 7, 2024
5a548f0
Merge branch 'v2_develop' into gradients
tznind Jul 7, 2024
0631579
Fix naming and tests compiler warnings
tznind Jul 7, 2024
1ef5645
Merge branch 'gradients' of https://github.com/tznind/gui.cs into gra…
tznind Jul 7, 2024
5240122
Merge branch 'v2_develop' into gradients
tznind Jul 7, 2024
8a56586
Fix note comment and add tests for SolidFill class
tznind Jul 7, 2024
afc0bf0
Fix dodgy constructor on FillPair and add tests
tznind Jul 7, 2024
d15f3af
Add tests that confirm LineCanvas behavior with Fill
tznind Jul 7, 2024
c62cd84
Add xml comment
tznind Jul 7, 2024
b9a8c7d
Fix GradientFill when not at origin
tznind Jul 7, 2024
18e1956
Make gradients flow like a flow layout
tznind Jul 7, 2024
04ca5fd
Merge branch 'v2_develop' into gradients
tznind Jul 7, 2024
2544d07
Merge branch 'v2_develop' into gradients
tznind Jul 8, 2024
ddaec2e
Merge branch 'v2_develop' into gradients
tig Jul 9, 2024
c981eef
Fix hanging xml comment
tznind Jul 9, 2024
1ba84de
Fix bad namespace
tznind Jul 9, 2024
fef6f33
Merge branch 'v2_develop' into gradients
tig Jul 9, 2024
65efddb
Added Border Settings.
tig Jul 9, 2024
d0f1280
Code cleanup
tig Jul 9, 2024
e09c0e6
Merge branch 'gradients' into tznind-gradients
tig Jul 9, 2024
f770bb9
Code cleanup2
tig Jul 9, 2024
097a800
Merge pull request #165 from tig/tznind-gradients
tznind Jul 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions Terminal.Gui/Drawing/FillPair.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Terminal.Gui;

/// <summary>
/// Describes a pair of <see cref="IFill"/> which cooperate in creating
/// <see cref="Attribute"/>. One gives foreground color while other gives background.
/// </summary>
public class FillPair
{
/// <summary>
/// Creates a new instance using the provided fills for foreground and background
/// color when assembling <see cref="Attribute"/>.
/// </summary>
/// <param name="fore"></param>
/// <param name="back"></param>
public FillPair (IFill fore, IFill back)
{
Foreground = fore;
Background = back;
}

/// <summary>
/// The fill which provides point based foreground color.
/// </summary>
public IFill Foreground { get; init; }

/// <summary>
/// The fill which provides point based background color.
/// </summary>
public IFill Background { get; init; }

/// <summary>
/// Returns the color pair (foreground+background) to use when rendering
/// a rune at the given <paramref name="point"/>.
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public Attribute GetAttribute (Point point)
{
return new (Foreground.GetColor (point), Background.GetColor (point));
}
}
255 changes: 255 additions & 0 deletions Terminal.Gui/Drawing/Gradient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// This code is a C# port from python library Terminal Text Effects https://github.com/ChrisBuilds/terminaltexteffects/

namespace Terminal.Gui;

/// <summary>
/// Describes the pattern that a <see cref="Gradient"/> results in e.g. <see cref="Vertical"/>,
/// <see cref="Horizontal"/> etc
/// </summary>
public enum GradientDirection
{
/// <summary>
/// Color varies along Y axis but is constant on X axis.
/// </summary>
Vertical,

/// <summary>
/// Color varies along X axis but is constant on Y axis.
/// </summary>
Horizontal,

tig marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Color varies by distance from center (i.e. in circular ripples)
/// </summary>
Radial,

/// <summary>
/// Color varies by X and Y axis (i.e. a slanted gradient)
/// </summary>
Diagonal
}

/// <summary>
/// Describes a <see cref="Spectrum"/> of colors that can be combined
/// to make a color gradient. Use <see cref="BuildCoordinateColorMapping"/>
/// to create into gradient fill area maps.
/// </summary>
public class Gradient
{
/// <summary>
/// The discrete colors that will make up the <see cref="Gradient"/>.
/// </summary>
public List<Color> Spectrum { get; }

private readonly bool _loop;
private readonly List<Color> _stops;
private readonly List<int> _steps;

/// <summary>
/// Creates a new instance of the <see cref="Gradient"/> class which hosts a <see cref="Spectrum"/>
/// of colors including all <paramref name="stops"/> and <paramref name="steps"/> interpolated colors
/// between each corresponding pair.
/// </summary>
/// <param name="stops">The colors to use in the spectrum (N)</param>
/// <param name="steps">
/// The number of colors to generate between each pair (must be N-1 numbers).
/// If only one step is passed then it is assumed to be the same distance for all pairs.
/// </param>
/// <param name="loop">True to duplicate the first stop and step so that the gradient repeats itself</param>
/// <exception cref="ArgumentException"></exception>
public Gradient (IEnumerable<Color> stops, IEnumerable<int> steps, bool loop = false)
{
_stops = stops.ToList ();

if (_stops.Count < 1)
{
throw new ArgumentException ("At least one color stop must be provided.");
}

_steps = steps.ToList ();

// If multiple colors and only 1 step assume same distance applies to all steps
if (_stops.Count > 2 && _steps.Count == 1)
{
_steps = Enumerable.Repeat (_steps.Single (), _stops.Count () - 1).ToList ();
}

if (_steps.Any (step => step < 1))
{
throw new ArgumentException ("Steps must be greater than 0.");
}

if (_steps.Count != _stops.Count - 1)
{
throw new ArgumentException ("Number of steps must be N-1");
}

_loop = loop;
Spectrum = GenerateGradient (_steps);
}

/// <summary>
/// Returns the color to use at the given part of the spectrum
/// </summary>
/// <param name="fraction">
/// Proportion of the way through the spectrum, must be between
/// 0 and 1 (inclusive). Returns the last color if <paramref name="fraction"/> is
/// <see cref="double.NaN"/>.
/// </param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public Color GetColorAtFraction (double fraction)
{
if (double.IsNaN (fraction))
{
return Spectrum.Last ();
}

if (fraction is < 0 or > 1)
{
throw new ArgumentOutOfRangeException (nameof (fraction), @"Fraction must be between 0 and 1.");
}

var index = (int)(fraction * (Spectrum.Count - 1));

return Spectrum [index];
}

private List<Color> GenerateGradient (IEnumerable<int> steps)
{
List<Color> gradient = new ();

if (_stops.Count == 1)
{
for (var i = 0; i < steps.Sum (); i++)
{
gradient.Add (_stops [0]);
}

return gradient;
}

List<Color> stopsToUse = _stops.ToList ();
List<int> stepsToUse = _steps.ToList ();

if (_loop)
{
stopsToUse.Add (_stops [0]);
stepsToUse.Add (_steps.First ());
}

var colorPairs = stopsToUse.Zip (stopsToUse.Skip (1), (start, end) => new { start, end });
List<int> stepsList = stepsToUse;

foreach ((var colorPair, int thesteps) in colorPairs.Zip (stepsList, (pair, step) => (pair, step)))
{
gradient.AddRange (InterpolateColors (colorPair.start, colorPair.end, thesteps));
}

return gradient;
}

private static IEnumerable<Color> InterpolateColors (Color start, Color end, int steps)
{
for (var step = 0; step < steps; step++)
{
double fraction = (double)step / steps;
var r = (int)(start.R + fraction * (end.R - start.R));
var g = (int)(start.G + fraction * (end.G - start.G));
var b = (int)(start.B + fraction * (end.B - start.B));

yield return new (r, g, b);
}

yield return end; // Ensure the last color is included
}

/// <summary>
/// <para>
/// Creates a mapping starting at 0,0 and going to <paramref name="maxRow"/> and <paramref name="maxColumn"/>
/// (inclusively) using the supplied <paramref name="direction"/>.
/// </para>
/// <para>
/// Note that this method is inclusive i.e. passing 1/1 results in 4 mapped coordinates.
/// </para>
/// </summary>
/// <param name="maxRow"></param>
/// <param name="maxColumn"></param>
/// <param name="direction"></param>
/// <returns></returns>
public Dictionary<Point, Color> BuildCoordinateColorMapping (int maxRow, int maxColumn, GradientDirection direction)
{
Dictionary<Point, Color> gradientMapping = new ();

switch (direction)
{
case GradientDirection.Vertical:
for (var row = 0; row <= maxRow; row++)
{
double fraction = maxRow == 0 ? 1.0 : (double)row / maxRow;
Color color = GetColorAtFraction (fraction);

for (var col = 0; col <= maxColumn; col++)
{
gradientMapping [new (col, row)] = color;
}
}

break;

case GradientDirection.Horizontal:
for (var col = 0; col <= maxColumn; col++)
{
double fraction = maxColumn == 0 ? 1.0 : (double)col / maxColumn;
Color color = GetColorAtFraction (fraction);

for (var row = 0; row <= maxRow; row++)
{
gradientMapping [new (col, row)] = color;
}
}

break;

case GradientDirection.Radial:
for (var row = 0; row <= maxRow; row++)
{
for (var col = 0; col <= maxColumn; col++)
{
double distanceFromCenter = FindNormalizedDistanceFromCenter (maxRow, maxColumn, new (col, row));
Color color = GetColorAtFraction (distanceFromCenter);
gradientMapping [new (col, row)] = color;
}
}

break;

case GradientDirection.Diagonal:
for (var row = 0; row <= maxRow; row++)
{
for (var col = 0; col <= maxColumn; col++)
{
double fraction = ((double)row * 2 + col) / (maxRow * 2 + maxColumn);
Color color = GetColorAtFraction (fraction);
gradientMapping [new (col, row)] = color;
}
}

break;
}

return gradientMapping;
}

private static double FindNormalizedDistanceFromCenter (int maxRow, int maxColumn, Point coord)
{
double centerX = maxColumn / 2.0;
double centerY = maxRow / 2.0;
double dx = coord.X - centerX;
double dy = coord.Y - centerY;
double distance = Math.Sqrt (dx * dx + dy * dy);
double maxDistance = Math.Sqrt (centerX * centerX + centerY * centerY);

return distance / maxDistance;
}
}
42 changes: 42 additions & 0 deletions Terminal.Gui/Drawing/GradientFill.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Terminal.Gui;

/// <summary>
/// Implementation of <see cref="IFill"/> that uses a color gradient (including
/// radial, diagonal etc.).
/// </summary>
public class GradientFill : IFill
{
private readonly Dictionary<Point, Color> _map;

/// <summary>
/// Creates a new instance of the <see cref="GradientFill"/> class that can return
/// color for any point in the given <paramref name="area"/> using the provided
/// <paramref name="gradient"/> and <paramref name="direction"/>.
/// </summary>
/// <param name="area"></param>
/// <param name="gradient"></param>
/// <param name="direction"></param>
public GradientFill (Rectangle area, Gradient gradient, GradientDirection direction)
{
_map = gradient.BuildCoordinateColorMapping (area.Height - 1, area.Width - 1, direction)
.ToDictionary (
kvp => new Point (kvp.Key.X + area.X, kvp.Key.Y + area.Y),
kvp => kvp.Value);
}

/// <summary>
/// Returns the color to use for the given <paramref name="point"/> or Black if it
/// lies outside the prepared gradient area (see constructor).
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public Color GetColor (Point point)
{
if (_map.TryGetValue (point, out Color color))
{
return color;
}

return new (0, 0); // Default to black if point not found
}
}
14 changes: 14 additions & 0 deletions Terminal.Gui/Drawing/IFill.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Terminal.Gui;

/// <summary>
/// Describes an area fill (e.g. solid color or gradient).
/// </summary>
public interface IFill
{
/// <summary>
/// Returns the color that should be used at the given point
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
Color GetColor (Point point);
}
Loading
Loading