using static Terminal.Gui.TextEffects.EventHandler; namespace Terminal.Gui.TextEffects; public enum SyncMetric { Distance, Step } public class CharacterVisual { public string Symbol { get; set; } public bool Bold { get; set; } public bool Dim { get; set; } public bool Italic { get; set; } public bool Underline { get; set; } public bool Blink { get; set; } public bool Reverse { get; set; } public bool Hidden { get; set; } public bool Strike { get; set; } public Color Color { get; set; } public string FormattedSymbol { get; private set; } private string _colorCode; // Holds the ANSI color code or similar string directly public string ColorCode => _colorCode; public CharacterVisual (string symbol, bool bold = false, bool dim = false, bool italic = false, bool underline = false, bool blink = false, bool reverse = false, bool hidden = false, bool strike = false, Color color = null, string colorCode = null) { Symbol = symbol; Bold = bold; Dim = dim; Italic = italic; Underline = underline; Blink = blink; Reverse = reverse; Hidden = hidden; Strike = strike; Color = color; _colorCode = colorCode; // Initialize _colorCode from the constructor argument FormattedSymbol = FormatSymbol (); } private string FormatSymbol () { string formattingString = ""; if (Bold) formattingString += Ansitools.ApplyBold (); if (Italic) formattingString += Ansitools.ApplyItalic (); if (Underline) formattingString += Ansitools.ApplyUnderline (); if (Blink) formattingString += Ansitools.ApplyBlink (); if (Reverse) formattingString += Ansitools.ApplyReverse (); if (Hidden) formattingString += Ansitools.ApplyHidden (); if (Strike) formattingString += Ansitools.ApplyStrikethrough (); if (_colorCode != null) formattingString += Colorterm.Fg (_colorCode); // Use the direct color code return $"{formattingString}{Symbol}{(formattingString != "" ? Ansitools.ResetAll () : "")}"; } public void DisableModes () { Bold = false; Dim = false; Italic = false; Underline = false; Blink = false; Reverse = false; Hidden = false; Strike = false; } } public class Frame { public CharacterVisual CharacterVisual { get; } public int Duration { get; } public int TicksElapsed { get; set; } public Frame (CharacterVisual characterVisual, int duration) { CharacterVisual = characterVisual; Duration = duration; TicksElapsed = 0; } public void IncrementTicks () { TicksElapsed++; } } public class Scene { public string SceneId { get; } public bool IsLooping { get; } public SyncMetric? Sync { get; } public EasingFunction Ease { get; } public bool NoColor { get; set; } public bool UseXtermColors { get; set; } public List Frames { get; } = new List (); public List PlayedFrames { get; } = new List (); public Dictionary FrameIndexMap { get; } = new Dictionary (); public int EasingTotalSteps { get; set; } public int EasingCurrentStep { get; set; } public static Dictionary XtermColorMap { get; } = new Dictionary (); public Scene (string sceneId, bool isLooping = false, SyncMetric? sync = null, EasingFunction ease = null, bool noColor = false, bool useXtermColors = false) { SceneId = sceneId; IsLooping = isLooping; Sync = sync; Ease = ease; NoColor = noColor; UseXtermColors = useXtermColors; EasingTotalSteps = 0; EasingCurrentStep = 0; } public void AddFrame (string symbol, int duration, Color color = null, bool bold = false, bool dim = false, bool italic = false, bool underline = false, bool blink = false, bool reverse = false, bool hidden = false, bool strike = false) { string charVisColor = null; if (color != null) { if (NoColor) { charVisColor = null; } else if (UseXtermColors && color.XtermColor.HasValue) { charVisColor = color.XtermColor.Value.ToString (); } else if (color.RgbColor != null && XtermColorMap.ContainsKey (color.RgbColor)) { charVisColor = XtermColorMap [color.RgbColor].ToString (); } else { charVisColor = color.RgbColor; } } if (duration < 1) throw new ArgumentException ("Duration must be greater than 0."); var characterVisual = new CharacterVisual (symbol, bold, dim, italic, underline, blink, reverse, hidden, strike, color, charVisColor); var frame = new Frame (characterVisual, duration); Frames.Add (frame); for (int i = 0; i < frame.Duration; i++) { FrameIndexMap [EasingTotalSteps] = frame; EasingTotalSteps++; } } public CharacterVisual Activate () { if (Frames.Count == 0) throw new InvalidOperationException ("Scene has no frames."); EasingCurrentStep = 0; return Frames [0].CharacterVisual; } public CharacterVisual GetNextVisual () { if (Frames.Count == 0) return null; var frame = Frames [0]; if (++EasingCurrentStep >= frame.Duration) { EasingCurrentStep = 0; PlayedFrames.Add (frame); Frames.RemoveAt (0); if (IsLooping && Frames.Count == 0) { Frames.AddRange (PlayedFrames); PlayedFrames.Clear (); } if (Frames.Count > 0) return Frames [0].CharacterVisual; } return frame.CharacterVisual; } public void ApplyGradientToSymbols (Gradient gradient, IList symbols, int duration) { int lastIndex = 0; for (int symbolIndex = 0; symbolIndex < symbols.Count; symbolIndex++) { var symbol = symbols [symbolIndex]; double symbolProgress = (symbolIndex + 1) / (double)symbols.Count; int gradientIndex = (int)(symbolProgress * gradient.Spectrum.Count); foreach (var color in gradient.Spectrum.GetRange (lastIndex, Math.Max (gradientIndex - lastIndex, 1))) { AddFrame (symbol, duration, color); } lastIndex = gradientIndex; } } public void ResetScene () { EasingCurrentStep = 0; Frames.Clear (); Frames.AddRange (PlayedFrames); PlayedFrames.Clear (); } public override bool Equals (object obj) { return obj is Scene other && SceneId == other.SceneId; } public override int GetHashCode () { return SceneId.GetHashCode (); } } public class Animation { public Dictionary Scenes { get; } = new Dictionary (); public EffectCharacter Character { get; } public Scene ActiveScene { get; private set; } public bool UseXtermColors { get; set; } = false; public bool NoColor { get; set; } = false; public Dictionary XtermColorMap { get; } = new Dictionary (); public int ActiveSceneCurrentStep { get; private set; } = 0; public CharacterVisual CurrentCharacterVisual { get; private set; } public Animation (EffectCharacter character) { Character = character; CurrentCharacterVisual = new CharacterVisual (character.InputSymbol); } public Scene NewScene (bool isLooping = false, SyncMetric? sync = null, EasingFunction ease = null, string id = "") { if (string.IsNullOrEmpty (id)) { bool foundUnique = false; int currentId = Scenes.Count; while (!foundUnique) { id = $"{Scenes.Count}"; if (!Scenes.ContainsKey (id)) { foundUnique = true; } else { currentId++; } } } var newScene = new Scene (id, isLooping, sync, ease); Scenes [id] = newScene; newScene.NoColor = NoColor; newScene.UseXtermColors = UseXtermColors; return newScene; } public Scene QueryScene (string sceneId) { if (!Scenes.TryGetValue (sceneId, out var scene)) { throw new ArgumentException ($"Scene {sceneId} does not exist."); } return scene; } public bool ActiveSceneIsComplete () { if (ActiveScene == null) { return true; } return ActiveScene.Frames.Count == 0 && !ActiveScene.IsLooping; } public void SetAppearance (string symbol, Color? color = null) { string charVisColor = null; if (color != null) { if (NoColor) { charVisColor = null; } else if (UseXtermColors) { charVisColor = color.XtermColor.ToString(); } else { charVisColor = color.RgbColor; } } CurrentCharacterVisual = new CharacterVisual (symbol, color: color, colorCode: charVisColor); } public static Color RandomColor () { var random = new Random (); var colorHex = random.Next (0, 0xFFFFFF).ToString ("X6"); return new Color (colorHex); } public static Color AdjustColorBrightness (Color color, float brightness) { float HueToRgb (float p, float q, float t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6f) return p + (q - p) * 6 * t; if (t < 1 / 2f) return q; if (t < 2 / 3f) return p + (q - p) * (2 / 3f - t) * 6; return p; } float r = int.Parse (color.RgbColor.Substring (0, 2), System.Globalization.NumberStyles.HexNumber) / 255f; float g = int.Parse (color.RgbColor.Substring (2, 2), System.Globalization.NumberStyles.HexNumber) / 255f; float b = int.Parse (color.RgbColor.Substring (4, 2), System.Globalization.NumberStyles.HexNumber) / 255f; float max = Math.Max (r, Math.Max (g, b)); float min = Math.Min (r, Math.Min (g, b)); float h, s, l = (max + min) / 2f; if (max == min) { h = s = 0; // achromatic } else { float d = max - min; s = l > 0.5f ? d / (2f - max - min) : d / (max + min); if (max == r) { h = (g - b) / d + (g < b ? 6 : 0); } else if (max == g) { h = (b - r) / d + 2; } else { h = (r - g) / d + 4; } h /= 6; } l = Math.Max (Math.Min (l * brightness, 1), 0); if (s == 0) { r = g = b = l; // achromatic } else { float q = l < 0.5f ? l * (1 + s) : l + s - l * s; float p = 2 * l - q; r = HueToRgb (p, q, h + 1 / 3f); g = HueToRgb (p, q, h); b = HueToRgb (p, q, h - 1 / 3f); } var adjustedColor = $"{(int)(r * 255):X2}{(int)(g * 255):X2}{(int)(b * 255):X2}"; return new Color (adjustedColor); } private float EaseAnimation (EasingFunction easingFunc) { if (ActiveScene == null) { return 0; } float elapsedStepRatio = ActiveScene.EasingCurrentStep / (float)ActiveScene.EasingTotalSteps; return easingFunc (elapsedStepRatio); } public void StepAnimation () { if (ActiveScene != null && ActiveScene.Frames.Count > 0) { if (ActiveScene.Sync != null) { if (Character.Motion.ActivePath != null) { int sequenceIndex = 0; if (ActiveScene.Sync == SyncMetric.Step) { sequenceIndex = (int)Math.Round ((ActiveScene.Frames.Count - 1) * (Math.Max (Character.Motion.ActivePath.CurrentStep, 1) / (float)Math.Max (Character.Motion.ActivePath.MaxSteps, 1))); } else if (ActiveScene.Sync == SyncMetric.Distance) { sequenceIndex = (int)Math.Round ((ActiveScene.Frames.Count - 1) * (Math.Max (Math.Max (Character.Motion.ActivePath.TotalDistance, 1) - Math.Max (Character.Motion.ActivePath.TotalDistance - Character.Motion.ActivePath.LastDistanceReached, 1), 1) / (float)Math.Max (Character.Motion.ActivePath.TotalDistance, 1))); } try { CurrentCharacterVisual = ActiveScene.Frames [sequenceIndex].CharacterVisual; } catch (IndexOutOfRangeException) { CurrentCharacterVisual = ActiveScene.Frames [^1].CharacterVisual; } } else { CurrentCharacterVisual = ActiveScene.Frames [^1].CharacterVisual; ActiveScene.PlayedFrames.AddRange (ActiveScene.Frames); ActiveScene.Frames.Clear (); } } else if (ActiveScene.Ease != null) { float easingFactor = EaseAnimation (ActiveScene.Ease); int frameIndex = (int)Math.Round (easingFactor * Math.Max (ActiveScene.EasingTotalSteps - 1, 0)); frameIndex = Math.Max (Math.Min (frameIndex, ActiveScene.EasingTotalSteps - 1), 0); Frame frame = ActiveScene.FrameIndexMap [frameIndex]; CurrentCharacterVisual = frame.CharacterVisual; ActiveScene.EasingCurrentStep++; if (ActiveScene.EasingCurrentStep == ActiveScene.EasingTotalSteps) { if (ActiveScene.IsLooping) { ActiveScene.EasingCurrentStep = 0; } else { ActiveScene.PlayedFrames.AddRange (ActiveScene.Frames); ActiveScene.Frames.Clear (); } } } else { CurrentCharacterVisual = ActiveScene.GetNextVisual (); } if (ActiveSceneIsComplete ()) { var completedScene = ActiveScene; if (!ActiveScene.IsLooping) { ActiveScene.ResetScene (); ActiveScene = null; } Character.EventHandler.HandleEvent (Event.SceneComplete, completedScene); } } } public void ActivateScene (Scene scene) { ActiveScene = scene; ActiveSceneCurrentStep = 0; CurrentCharacterVisual = ActiveScene.Activate (); Character.EventHandler.HandleEvent (Event.SceneActivated, scene); } public void DeactivateScene (Scene scene) { if (ActiveScene == scene) { ActiveScene = null; } } } // Dummy classes for Ansitools, Colorterm, and Hexterm as placeholders public static class Ansitools { public static string ApplyBold () => "\x1b[1m"; public static string ApplyItalic () => "\x1b[3m"; public static string ApplyUnderline () => "\x1b[4m"; public static string ApplyBlink () => "\x1b[5m"; public static string ApplyReverse () => "\x1b[7m"; public static string ApplyHidden () => "\x1b[8m"; public static string ApplyStrikethrough () => "\x1b[9m"; public static string ResetAll () => "\x1b[0m"; } public static class Colorterm { public static string Fg (string colorCode) => $"\x1b[38;5;{colorCode}m"; } public static class Hexterm { public static string HexToXterm (string hex) { // Convert hex color to xterm color code (0-255) return "15"; // Example output } }