Radial gradient

This commit is contained in:
tznind
2024-07-06 20:39:01 +01:00
parent d7a4e0e7c1
commit ab07f53bd2
6 changed files with 733 additions and 46 deletions

View File

@@ -0,0 +1,273 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Terminal.Gui.TextEffects;
using Color = Terminal.Gui.TextEffects.Color;
public static class PositiveInt
{
public static int Parse (string arg)
{
if (int.TryParse (arg, out int value) && value > 0)
{
return value;
}
else
{
throw new ArgumentException ($"invalid value: '{arg}' is not > 0.");
}
}
}
public static class NonNegativeInt
{
public static int Parse (string arg)
{
if (int.TryParse (arg, out int value) && value >= 0)
{
return value;
}
else
{
throw new ArgumentException ($"invalid value: '{arg}' Argument must be int >= 0.");
}
}
}
public static class IntRange
{
public static (int, int) Parse (string arg)
{
var parts = arg.Split ('-');
if (parts.Length == 2 && int.TryParse (parts [0], out int start) && int.TryParse (parts [1], out int end) && start > 0 && start <= end)
{
return (start, end);
}
else
{
throw new ArgumentException ($"invalid range: '{arg}' is not a valid range. Must be start-end. Ex: 1-10");
}
}
}
public static class PositiveFloat
{
public static float Parse (string arg)
{
if (float.TryParse (arg, out float value) && value > 0)
{
return value;
}
else
{
throw new ArgumentException ($"invalid value: '{arg}' is not a valid value. Argument must be a float > 0.");
}
}
}
public static class NonNegativeFloat
{
public static float Parse (string arg)
{
if (float.TryParse (arg, out float value) && value >= 0)
{
return value;
}
else
{
throw new ArgumentException ($"invalid argument value: '{arg}' is out of range. Must be float >= 0.");
}
}
}
public static class PositiveFloatRange
{
public static (float, float) Parse (string arg)
{
var parts = arg.Split ('-');
if (parts.Length == 2 && float.TryParse (parts [0], out float start) && float.TryParse (parts [1], out float end) && start > 0 && start <= end)
{
return (start, end);
}
else
{
throw new ArgumentException ($"invalid range: '{arg}' is not a valid range. Must be start-end. Ex: 0.1-1.0");
}
}
}
public static class Ratio
{
public static float Parse (string arg)
{
if (float.TryParse (arg, out float value) && value >= 0 && value <= 1)
{
return value;
}
else
{
throw new ArgumentException ($"invalid value: '{arg}' is not a float >= 0 and <= 1. Example: 0.5");
}
}
}
public enum GradientDirection
{
Horizontal,
Vertical,
Diagonal,
Radial
}
public static class GradientDirectionParser
{
public static GradientDirection Parse (string arg)
{
return arg.ToLower () switch
{
"horizontal" => GradientDirection.Horizontal,
"vertical" => GradientDirection.Vertical,
"diagonal" => GradientDirection.Diagonal,
"radial" => GradientDirection.Radial,
_ => throw new ArgumentException ($"invalid gradient direction: '{arg}' is not a valid gradient direction. Choices are diagonal, horizontal, vertical, or radial."),
};
}
}
public static class ColorArg
{
public static Color Parse (string arg)
{
if (int.TryParse (arg, out int xtermValue) && xtermValue >= 0 && xtermValue <= 255)
{
return new Color (xtermValue);
}
else if (arg.Length == 6 && int.TryParse (arg, NumberStyles.HexNumber, null, out int _))
{
return new Color (arg);
}
else
{
throw new ArgumentException ($"invalid color value: '{arg}' is not a valid XTerm or RGB color. Must be in range 0-255 or 000000-FFFFFF.");
}
}
}
public static class Symbol
{
public static string Parse (string arg)
{
if (arg.Length == 1 && IsAsciiOrUtf8 (arg))
{
return arg;
}
else
{
throw new ArgumentException ($"invalid symbol: '{arg}' is not a valid symbol. Must be a single ASCII/UTF-8 character.");
}
}
private static bool IsAsciiOrUtf8 (string s)
{
try
{
Encoding.ASCII.GetBytes (s);
}
catch (EncoderFallbackException)
{
try
{
Encoding.UTF8.GetBytes (s);
}
catch (EncoderFallbackException)
{
return false;
}
}
return true;
}
}
public static class CanvasDimension
{
public static int Parse (string arg)
{
if (int.TryParse (arg, out int value) && value >= -1)
{
return value;
}
else
{
throw new ArgumentException ($"invalid value: '{arg}' is not >= -1.");
}
}
}
public static class TerminalDimensions
{
public static (int, int) Parse (string arg)
{
var parts = arg.Split (' ');
if (parts.Length == 2 && int.TryParse (parts [0], out int width) && int.TryParse (parts [1], out int height) && width >= 0 && height >= 0)
{
return (width, height);
}
else
{
throw new ArgumentException ($"invalid terminal dimensions: '{arg}' is not a valid terminal dimension. Must be >= 0.");
}
}
}
public static class Ease
{
private static readonly Dictionary<string, EasingFunction> easingFuncMap = new ()
{
{"linear", Easing.Linear},
{"in_sine", Easing.InSine},
{"out_sine", Easing.OutSine},
{"in_out_sine", Easing.InOutSine},
{"in_quad", Easing.InQuad},
{"out_quad", Easing.OutQuad},
{"in_out_quad", Easing.InOutQuad},
{"in_cubic", Easing.InCubic},
{"out_cubic", Easing.OutCubic},
{"in_out_cubic", Easing.InOutCubic},
{"in_quart", Easing.InQuart},
{"out_quart", Easing.OutQuart},
{"in_out_quart", Easing.InOutQuart},
{"in_quint", Easing.InQuint},
{"out_quint", Easing.OutQuint},
{"in_out_quint", Easing.InOutQuint},
{"in_expo", Easing.InExpo},
{"out_expo", Easing.OutExpo},
{"in_out_expo", Easing.InOutExpo},
{"in_circ", Easing.InCirc},
{"out_circ", Easing.OutCirc},
{"in_out_circ", Easing.InOutCirc},
{"in_back", Easing.InBack},
{"out_back", Easing.OutBack},
{"in_out_back", Easing.InOutBack},
{"in_elastic", Easing.InElastic},
{"out_elastic", Easing.OutElastic},
{"in_out_elastic", Easing.InOutElastic},
{"in_bounce", Easing.InBounce},
{"out_bounce", Easing.OutBounce},
{"in_out_bounce", Easing.InOutBounce},
};
public static EasingFunction Parse (string arg)
{
if (easingFuncMap.TryGetValue (arg.ToLower (), out var easingFunc))
{
return easingFunc;
}
else
{
throw new ArgumentException ($"invalid ease value: '{arg}' is not a valid ease.");
}
}
}

View File

@@ -6,10 +6,16 @@ public abstract class BaseEffectIterator<T> where T : EffectConfig, new()
protected Terminal Terminal { get; set; }
protected List<EffectCharacter> ActiveCharacters { get; set; } = new List<EffectCharacter> ();
protected BaseEffect<T> Effect { get; }
public BaseEffectIterator (BaseEffect<T> effect)
{
Effect = effect;
Config = effect.EffectConfig;
Terminal = new Terminal (effect.InputData, effect.TerminalConfig);
}
public void Update ()

View File

@@ -0,0 +1,241 @@
/*namespace Terminal.Gui.TextEffects.Effects;
public class BeamsConfig : EffectConfig
{
public string [] BeamRowSymbols { get; set; } = { "▂", "▁", "_" };
public string [] BeamColumnSymbols { get; set; } = { "▌", "▍", "▎", "▏" };
public int BeamDelay { get; set; } = 10;
public (int, int) BeamRowSpeedRange { get; set; } = (10, 40);
public (int, int) BeamColumnSpeedRange { get; set; } = (6, 10);
public Color [] BeamGradientStops { get; set; } = { new Color ("ffffff"), new Color ("00D1FF"), new Color ("8A008A") };
public int [] BeamGradientSteps { get; set; } = { 2, 8 };
public int BeamGradientFrames { get; set; } = 2;
public Color [] FinalGradientStops { get; set; } = { new Color ("8A008A"), new Color ("00D1FF"), new Color ("ffffff") };
public int [] FinalGradientSteps { get; set; } = { 12 };
public int FinalGradientFrames { get; set; } = 5;
public GradientDirection FinalGradientDirection { get; set; } = GradientDirection.Vertical;
public int FinalWipeSpeed { get; set; } = 1;
}
public class Beams : BaseEffect<BeamsConfig>
{
public Beams (string inputData) : base (inputData)
{
}
protected override BaseEffectIterator<BeamsConfig> CreateIterator ()
{
return new BeamsIterator (this);
}
}
public class BeamsIterator : BaseEffectIterator<BeamsConfig>
{
private class Group
{
public List<EffectCharacter> Characters { get; private set; }
public string Direction { get; private set; }
private Terminal Terminal;
private BeamsConfig Config;
private double Speed;
private float NextCharacterCounter;
private List<EffectCharacter> SortedCharacters;
public Group (List<EffectCharacter> characters, string direction, Terminal terminal, BeamsConfig config)
{
Characters = characters;
Direction = direction;
Terminal = terminal;
Config = config;
Speed = new Random ().Next (config.BeamRowSpeedRange.Item1, config.BeamRowSpeedRange.Item2) * 0.1;
NextCharacterCounter = 0;
SortedCharacters = direction == "row"
? characters.OrderBy (c => c.InputCoord.Column).ToList ()
: characters.OrderBy (c => c.InputCoord.Row).ToList ();
if (new Random ().Next (0, 2) == 0)
{
SortedCharacters.Reverse ();
}
}
public void IncrementNextCharacterCounter ()
{
NextCharacterCounter += (float)Speed;
}
public EffectCharacter GetNextCharacter ()
{
NextCharacterCounter -= 1;
var nextCharacter = SortedCharacters.First ();
SortedCharacters.RemoveAt (0);
if (nextCharacter.Animation.ActiveScene != null)
{
nextCharacter.Animation.ActiveScene.ResetScene ();
return null;
}
Terminal.SetCharacterVisibility (nextCharacter, true);
nextCharacter.Animation.ActivateScene (nextCharacter.Animation.QueryScene ("beam_" + Direction));
return nextCharacter;
}
public bool Complete ()
{
return !SortedCharacters.Any ();
}
}
private List<Group> PendingGroups = new List<Group> ();
private Dictionary<EffectCharacter, Color> CharacterFinalColorMap = new Dictionary<EffectCharacter, Color> ();
private List<Group> ActiveGroups = new List<Group> ();
private int Delay = 0;
private string Phase = "beams";
private List<List<EffectCharacter>> FinalWipeGroups;
public BeamsIterator (Beams effect) : base (effect)
{
Build ();
}
private void Build ()
{
var finalGradient = new Gradient (Effect.Config.FinalGradientStops, Effect.Config.FinalGradientSteps);
var finalGradientMapping = finalGradient.BuildCoordinateColorMapping (
Effect.Terminal.Canvas.Top,
Effect.Terminal.Canvas.Right,
Effect.Config.FinalGradientDirection
);
foreach (var character in Effect.Terminal.GetCharacters (fillChars: true))
{
CharacterFinalColorMap [character] = finalGradientMapping [character.InputCoord];
}
var beamGradient = new Gradient (Effect.Config.BeamGradientStops, Effect.Config.BeamGradientSteps);
var groups = new List<Group> ();
foreach (var row in Effect.Terminal.GetCharactersGrouped (Terminal.CharacterGroup.RowTopToBottom, fillChars: true))
{
groups.Add (new Group (row, "row", Effect.Terminal, Effect.Config));
}
foreach (var column in Effect.Terminal.GetCharactersGrouped (Terminal.CharacterGroup.ColumnLeftToRight, fillChars: true))
{
groups.Add (new Group (column, "column", Effect.Terminal, Effect.Config));
}
foreach (var group in groups)
{
foreach (var character in group.Characters)
{
var beamRowScene = character.Animation.NewScene (id: "beam_row");
var beamColumnScene = character.Animation.NewScene (id: "beam_column");
beamRowScene.ApplyGradientToSymbols (
beamGradient, Effect.Config.BeamRowSymbols, Effect.Config.BeamGradientFrames);
beamColumnScene.ApplyGradientToSymbols (
beamGradient, Effect.Config.BeamColumnSymbols, Effect.Config.BeamGradientFrames);
var fadedColor = character.Animation.AdjustColorBrightness (CharacterFinalColorMap [character], 0.3f);
var fadeGradient = new Gradient (CharacterFinalColorMap [character], fadedColor, steps: 10);
beamRowScene.ApplyGradientToSymbols (fadeGradient, character.InputSymbol, 5);
beamColumnScene.ApplyGradientToSymbols (fadeGradient, character.InputSymbol, 5);
var brightenGradient = new Gradient (fadedColor, CharacterFinalColorMap [character], steps: 10);
var brightenScene = character.Animation.NewScene (id: "brighten");
brightenScene.ApplyGradientToSymbols (
brightenGradient, character.InputSymbol, Effect.Config.FinalGradientFrames);
}
}
PendingGroups = groups;
new Random ().Shuffle (PendingGroups);
}
public override bool MoveNext ()
{
if (Phase != "complete" || ActiveCharacters.Any ())
{
if (Phase == "beams")
{
if (Delay == 0)
{
if (PendingGroups.Any ())
{
for (int i = 0; i < new Random ().Next (1, 6); i++)
{
if (PendingGroups.Any ())
{
ActiveGroups.Add (PendingGroups.First ());
PendingGroups.RemoveAt (0);
}
}
}
Delay = Effect.Config.BeamDelay;
}
else
{
Delay--;
}
foreach (var group in ActiveGroups)
{
group.IncrementNextCharacterCounter ();
if ((int)group.NextCharacterCounter > 1)
{
for (int i = 0; i < (int)group.NextCharacterCounter; i++)
{
if (!group.Complete ())
{
var nextChar = group.GetNextCharacter ();
if (nextChar != null)
{
ActiveCharacters.Add (nextChar);
}
}
}
}
}
ActiveGroups = ActiveGroups.Where (g => !g.Complete ()).ToList ();
if (!PendingGroups.Any () && !ActiveGroups.Any () && !ActiveCharacters.Any ())
{
Phase = "final_wipe";
}
}
else if (Phase == "final_wipe")
{
if (FinalWipeGroups.Any ())
{
for (int i = 0; i < Effect.Config.FinalWipeSpeed; i++)
{
if (!FinalWipeGroups.Any ()) break;
var nextGroup = FinalWipeGroups.First ();
FinalWipeGroups.RemoveAt (0);
foreach (var character in nextGroup)
{
character.Animation.ActivateScene (character.Animation.QueryScene ("brighten"));
Effect.Terminal.SetCharacterVisibility (character, true);
ActiveCharacters.Add (character);
}
}
}
else
{
Phase = "complete";
}
}
Update ();
return true;
}
else
{
return false;
}
}
}
*/

View File

@@ -54,51 +54,33 @@ public class Color
}
}
public class Gradient
{
public List<Color> Spectrum { get; private set; }
private readonly bool _loop;
private readonly List<Color> _stops;
private readonly List<int> _steps;
public enum Direction
{
Vertical,
Horizontal,
Radial,
Diagonal
}
// Constructor now accepts IEnumerable<int> for steps.
public Gradient (IEnumerable<Color> stops, IEnumerable<int> steps, bool loop = false)
{
if (stops == null || !stops.Any () || stops.Count () < 2)
throw new ArgumentException ("At least two color stops are required to create a gradient.");
if (steps == null || !steps.Any ())
throw new ArgumentException ("Steps are required to define the transitions between colors.");
_stops = stops.ToList ();
if (_stops.Count < 1)
throw new ArgumentException ("At least one color stop must be provided.");
Spectrum = GenerateGradient (stops.ToList (), steps.ToList (), loop);
}
_steps = steps.ToList ();
if (_steps.Any (step => step < 1))
throw new ArgumentException ("Steps must be greater than 0.");
private List<Color> GenerateGradient (List<Color> stops, List<int> steps, bool loop)
{
List<Color> gradient = new List<Color> ();
if (loop)
stops.Add (stops [0]); // Loop the gradient back to the first color.
for (int i = 0; i < stops.Count - 1; i++)
{
int currentSteps = i < steps.Count ? steps [i] : steps.Last ();
gradient.AddRange (InterpolateColors (stops [i], stops [i + 1], currentSteps));
}
return gradient;
}
private IEnumerable<Color> InterpolateColors (Color start, Color end, int steps)
{
for (int step = 0; step <= steps; step++)
{
int r = Interpolate (start.R, end.R, steps, step);
int g = Interpolate (start.G, end.G, steps, step);
int b = Interpolate (start.B, end.B, steps, step);
yield return Color.FromRgb (r, g, b);
}
}
private int Interpolate (int start, int end, int steps, int currentStep)
{
return start + (int)((end - start) * (double)currentStep / steps);
_loop = loop;
Spectrum = GenerateGradient (_steps);
}
public Color GetColorAtFraction (double fraction)
@@ -108,6 +90,113 @@ public class Gradient
int index = (int)(fraction * (Spectrum.Count - 1));
return Spectrum [index];
}
}
private List<Color> GenerateGradient (IEnumerable<int> steps)
{
List<Color> gradient = new List<Color> ();
if (_stops.Count == 1)
{
for (int i = 0; i < steps.Sum (); i++)
{
gradient.Add (_stops [0]);
}
return gradient;
}
if (_loop)
{
_stops.Add (_stops [0]);
}
var colorPairs = _stops.Zip (_stops.Skip (1), (start, end) => new { start, end });
var stepsList = _steps.ToList ();
foreach (var (colorPair, thesteps) in colorPairs.Zip (stepsList, (pair, step) => (pair, step)))
{
gradient.AddRange (InterpolateColors (colorPair.start, colorPair.end, thesteps));
}
return gradient;
}
private IEnumerable<Color> InterpolateColors (Color start, Color end, int steps)
{
for (int step = 0; step <= steps; step++)
{
double fraction = (double)step / steps;
int r = (int)(start.R + fraction * (end.R - start.R));
int g = (int)(start.G + fraction * (end.G - start.G));
int b = (int)(start.B + fraction * (end.B - start.B));
yield return Color.FromRgb (r, g, b);
}
}
public Dictionary<Coord, Color> BuildCoordinateColorMapping (int maxRow, int maxColumn, Direction direction)
{
var gradientMapping = new Dictionary<Coord, Color> ();
switch (direction)
{
case Direction.Vertical:
for (int row = 0; row <= maxRow; row++)
{
double fraction = maxRow == 0 ? 1.0 : (double)row / maxRow;
Color color = GetColorAtFraction (fraction);
for (int col = 0; col <= maxColumn; col++)
{
gradientMapping [new Coord (col, row)] = color;
}
}
break;
case Direction.Horizontal:
for (int col = 0; col <= maxColumn; col++)
{
double fraction = maxColumn == 0 ? 1.0 : (double)col / maxColumn;
Color color = GetColorAtFraction (fraction);
for (int row = 0; row <= maxRow; row++)
{
gradientMapping [new Coord (col, row)] = color;
}
}
break;
case Direction.Radial:
for (int row = 0; row <= maxRow; row++)
{
for (int col = 0; col <= maxColumn; col++)
{
double distanceFromCenter = FindNormalizedDistanceFromCenter (maxRow, maxColumn, new Coord (col, row));
Color color = GetColorAtFraction (distanceFromCenter);
gradientMapping [new Coord (col, row)] = color;
}
}
break;
case Direction.Diagonal:
for (int row = 0; row <= maxRow; row++)
{
for (int col = 0; col <= maxColumn; col++)
{
double fraction = ((double)row * 2 + col) / ((maxRow * 2) + maxColumn);
Color color = GetColorAtFraction (fraction);
gradientMapping [new Coord (col, row)] = color;
}
}
break;
}
return gradientMapping;
}
private double FindNormalizedDistanceFromCenter (int maxRow, int maxColumn, Coord coord)
{
double centerX = maxColumn / 2.0;
double centerY = maxRow / 2.0;
double dx = coord.Column - centerX;
double dy = coord.Row - centerY;
double distance = Math.Sqrt (dx * dx + dy * dy);
double maxDistance = Math.Sqrt (centerX * centerX + centerY * centerY);
return distance / maxDistance;
}
}

View File

@@ -1,5 +1,4 @@
namespace Terminal.Gui.TextEffects;
public class Coord
{
public int Column { get; set; }
@@ -12,6 +11,34 @@ public class Coord
}
public override string ToString () => $"({Column}, {Row})";
public override bool Equals (object obj)
{
if (obj is Coord other)
{
return Column == other.Column && Row == other.Row;
}
return false;
}
public override int GetHashCode ()
{
return HashCode.Combine (Column, Row);
}
public static bool operator == (Coord left, Coord right)
{
if (left is null)
{
return right is null;
}
return left.Equals (right);
}
public static bool operator != (Coord left, Coord right)
{
return !(left == right);
}
}
public class Waypoint

View File

@@ -84,6 +84,53 @@ internal class TextEffectsExampleView : View
resized = false;
}
DrawTopLineGradient (viewport);
DrawRadialGradient (viewport);
_ball?.Draw ();
}
private void DrawRadialGradient (Rectangle viewport)
{
// Define the colors of the gradient stops
var stops = new List<Color>
{
Color.FromRgb(255, 0, 0), // Red
Color.FromRgb(0, 255, 0), // Green
Color.FromRgb(238, 130, 238) // Violet
};
// Define the number of steps between each color
var steps = new List<int> { 10, 10 }; // 10 steps between Red -> Green, and Green -> Blue
// Create the gradient
var radialGradient = new Gradient (stops, steps, loop: false);
// Define the size of the rectangle
int maxRow = 20;
int maxColumn = 40;
// Build the coordinate-color mapping for a radial gradient
var gradientMapping = radialGradient.BuildCoordinateColorMapping (maxRow, maxColumn, Gradient.Direction.Radial);
// Print the gradient
for (int row = 0; row <= maxRow; row++)
{
for (int col = 0; col <= maxColumn; col++)
{
var coord = new Coord (col, row);
var color = gradientMapping [coord];
SetColor (color);
AddRune (col+2, row+3, new Rune ('█'));
}
}
}
private void DrawTopLineGradient (Rectangle viewport)
{
// Define the colors of the rainbow
var stops = new List<Color>
{
@@ -115,17 +162,21 @@ internal class TextEffectsExampleView : View
double fraction = (double)x / (viewport.Width - 1);
Color color = rainbowGradient.GetColorAtFraction (fraction);
// Assuming AddRune is a method you have for drawing at specific positions
Application.Driver.SetAttribute (
new Attribute (
new Terminal.Gui.Color (color.R, color.G, color.B),
new Terminal.Gui.Color (color.R, color.G, color.B)
)); // Setting color based on RGB
SetColor (color);
AddRune (x, 0, new Rune ('█'));
}
}
private void SetColor (Color color)
{
// Assuming AddRune is a method you have for drawing at specific positions
Application.Driver.SetAttribute (
new Attribute (
new Terminal.Gui.Color (color.R, color.G, color.B),
new Terminal.Gui.Color (color.R, color.G, color.B)
)); // Setting color based on RGB
_ball?.Draw ();
}
public class Ball