Animation.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. 
  2. using static Terminal.Gui.TextEffects.EventHandler;
  3. namespace Terminal.Gui.TextEffects;
  4. public enum SyncMetric
  5. {
  6. Distance,
  7. Step
  8. }
  9. public class CharacterVisual
  10. {
  11. public string Symbol { get; set; }
  12. public bool Bold { get; set; }
  13. public bool Dim { get; set; }
  14. public bool Italic { get; set; }
  15. public bool Underline { get; set; }
  16. public bool Blink { get; set; }
  17. public bool Reverse { get; set; }
  18. public bool Hidden { get; set; }
  19. public bool Strike { get; set; }
  20. public Color Color { get; set; }
  21. public string FormattedSymbol { get; private set; }
  22. private string _colorCode; // Holds the ANSI color code or similar string directly
  23. public string ColorCode => _colorCode;
  24. 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)
  25. {
  26. Symbol = symbol;
  27. Bold = bold;
  28. Dim = dim;
  29. Italic = italic;
  30. Underline = underline;
  31. Blink = blink;
  32. Reverse = reverse;
  33. Hidden = hidden;
  34. Strike = strike;
  35. Color = color;
  36. _colorCode = colorCode; // Initialize _colorCode from the constructor argument
  37. FormattedSymbol = FormatSymbol ();
  38. }
  39. private string FormatSymbol ()
  40. {
  41. string formattingString = "";
  42. if (Bold) formattingString += Ansitools.ApplyBold ();
  43. if (Italic) formattingString += Ansitools.ApplyItalic ();
  44. if (Underline) formattingString += Ansitools.ApplyUnderline ();
  45. if (Blink) formattingString += Ansitools.ApplyBlink ();
  46. if (Reverse) formattingString += Ansitools.ApplyReverse ();
  47. if (Hidden) formattingString += Ansitools.ApplyHidden ();
  48. if (Strike) formattingString += Ansitools.ApplyStrikethrough ();
  49. if (_colorCode != null) formattingString += Colorterm.Fg (_colorCode); // Use the direct color code
  50. return $"{formattingString}{Symbol}{(formattingString != "" ? Ansitools.ResetAll () : "")}";
  51. }
  52. public void DisableModes ()
  53. {
  54. Bold = false;
  55. Dim = false;
  56. Italic = false;
  57. Underline = false;
  58. Blink = false;
  59. Reverse = false;
  60. Hidden = false;
  61. Strike = false;
  62. }
  63. }
  64. public class Frame
  65. {
  66. public CharacterVisual CharacterVisual { get; }
  67. public int Duration { get; }
  68. public int TicksElapsed { get; set; }
  69. public Frame (CharacterVisual characterVisual, int duration)
  70. {
  71. CharacterVisual = characterVisual;
  72. Duration = duration;
  73. TicksElapsed = 0;
  74. }
  75. public void IncrementTicks ()
  76. {
  77. TicksElapsed++;
  78. }
  79. }
  80. public class Scene
  81. {
  82. public string SceneId { get; }
  83. public bool IsLooping { get; }
  84. public SyncMetric? Sync { get; }
  85. public EasingFunction Ease { get; }
  86. public bool NoColor { get; set; }
  87. public bool UseXtermColors { get; set; }
  88. public List<Frame> Frames { get; } = new List<Frame> ();
  89. public List<Frame> PlayedFrames { get; } = new List<Frame> ();
  90. public Dictionary<int, Frame> FrameIndexMap { get; } = new Dictionary<int, Frame> ();
  91. public int EasingTotalSteps { get; set; }
  92. public int EasingCurrentStep { get; set; }
  93. public static Dictionary<string, int> XtermColorMap { get; } = new Dictionary<string, int> ();
  94. public Scene (string sceneId, bool isLooping = false, SyncMetric? sync = null, EasingFunction ease = null, bool noColor = false, bool useXtermColors = false)
  95. {
  96. SceneId = sceneId;
  97. IsLooping = isLooping;
  98. Sync = sync;
  99. Ease = ease;
  100. NoColor = noColor;
  101. UseXtermColors = useXtermColors;
  102. EasingTotalSteps = 0;
  103. EasingCurrentStep = 0;
  104. }
  105. 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)
  106. {
  107. string charVisColor = null;
  108. if (color != null)
  109. {
  110. if (NoColor)
  111. {
  112. charVisColor = null;
  113. }
  114. else if (UseXtermColors && color.XtermColor.HasValue)
  115. {
  116. charVisColor = color.XtermColor.Value.ToString ();
  117. }
  118. else if (color.RgbColor != null && XtermColorMap.ContainsKey (color.RgbColor))
  119. {
  120. charVisColor = XtermColorMap [color.RgbColor].ToString ();
  121. }
  122. else
  123. {
  124. charVisColor = color.RgbColor;
  125. }
  126. }
  127. if (duration < 1)
  128. throw new ArgumentException ("Duration must be greater than 0.");
  129. var characterVisual = new CharacterVisual (symbol, bold, dim, italic, underline, blink, reverse, hidden, strike, color, charVisColor);
  130. var frame = new Frame (characterVisual, duration);
  131. Frames.Add (frame);
  132. for (int i = 0; i < frame.Duration; i++)
  133. {
  134. FrameIndexMap [EasingTotalSteps] = frame;
  135. EasingTotalSteps++;
  136. }
  137. }
  138. public CharacterVisual Activate ()
  139. {
  140. if (Frames.Count == 0)
  141. throw new InvalidOperationException ("Scene has no frames.");
  142. EasingCurrentStep = 0;
  143. return Frames [0].CharacterVisual;
  144. }
  145. public CharacterVisual GetNextVisual ()
  146. {
  147. if (Frames.Count == 0)
  148. return null;
  149. var frame = Frames [0];
  150. if (++EasingCurrentStep >= frame.Duration)
  151. {
  152. EasingCurrentStep = 0;
  153. PlayedFrames.Add (frame);
  154. Frames.RemoveAt (0);
  155. if (IsLooping && Frames.Count == 0)
  156. {
  157. Frames.AddRange (PlayedFrames);
  158. PlayedFrames.Clear ();
  159. }
  160. if (Frames.Count > 0)
  161. return Frames [0].CharacterVisual;
  162. }
  163. return frame.CharacterVisual;
  164. }
  165. public void ApplyGradientToSymbols (Gradient gradient, IList<string> symbols, int duration)
  166. {
  167. int lastIndex = 0;
  168. for (int symbolIndex = 0; symbolIndex < symbols.Count; symbolIndex++)
  169. {
  170. var symbol = symbols [symbolIndex];
  171. double symbolProgress = (symbolIndex + 1) / (double)symbols.Count;
  172. int gradientIndex = (int)(symbolProgress * gradient.Spectrum.Count);
  173. foreach (var color in gradient.Spectrum.GetRange (lastIndex, Math.Max (gradientIndex - lastIndex, 1)))
  174. {
  175. AddFrame (symbol, duration, color);
  176. }
  177. lastIndex = gradientIndex;
  178. }
  179. }
  180. public void ResetScene ()
  181. {
  182. EasingCurrentStep = 0;
  183. Frames.Clear ();
  184. Frames.AddRange (PlayedFrames);
  185. PlayedFrames.Clear ();
  186. }
  187. public override bool Equals (object obj)
  188. {
  189. return obj is Scene other && SceneId == other.SceneId;
  190. }
  191. public override int GetHashCode ()
  192. {
  193. return SceneId.GetHashCode ();
  194. }
  195. }
  196. public class Animation
  197. {
  198. public Dictionary<string, Scene> Scenes { get; } = new Dictionary<string, Scene> ();
  199. public EffectCharacter Character { get; }
  200. public Scene ActiveScene { get; private set; }
  201. public bool UseXtermColors { get; set; } = false;
  202. public bool NoColor { get; set; } = false;
  203. public Dictionary<string, int> XtermColorMap { get; } = new Dictionary<string, int> ();
  204. public int ActiveSceneCurrentStep { get; private set; } = 0;
  205. public CharacterVisual CurrentCharacterVisual { get; private set; }
  206. public Animation (EffectCharacter character)
  207. {
  208. Character = character;
  209. CurrentCharacterVisual = new CharacterVisual (character.InputSymbol);
  210. }
  211. public Scene NewScene (bool isLooping = false, SyncMetric? sync = null, EasingFunction ease = null, string id = "")
  212. {
  213. if (string.IsNullOrEmpty (id))
  214. {
  215. bool foundUnique = false;
  216. int currentId = Scenes.Count;
  217. while (!foundUnique)
  218. {
  219. id = $"{Scenes.Count}";
  220. if (!Scenes.ContainsKey (id))
  221. {
  222. foundUnique = true;
  223. }
  224. else
  225. {
  226. currentId++;
  227. }
  228. }
  229. }
  230. var newScene = new Scene (id, isLooping, sync, ease);
  231. Scenes [id] = newScene;
  232. newScene.NoColor = NoColor;
  233. newScene.UseXtermColors = UseXtermColors;
  234. return newScene;
  235. }
  236. public Scene QueryScene (string sceneId)
  237. {
  238. if (!Scenes.TryGetValue (sceneId, out var scene))
  239. {
  240. throw new ArgumentException ($"Scene {sceneId} does not exist.");
  241. }
  242. return scene;
  243. }
  244. public bool ActiveSceneIsComplete ()
  245. {
  246. if (ActiveScene == null)
  247. {
  248. return true;
  249. }
  250. return ActiveScene.Frames.Count == 0 && !ActiveScene.IsLooping;
  251. }
  252. public void SetAppearance (string symbol, Color? color = null)
  253. {
  254. string charVisColor = null;
  255. if (color != null)
  256. {
  257. if (NoColor)
  258. {
  259. charVisColor = null;
  260. }
  261. else if (UseXtermColors)
  262. {
  263. charVisColor = color.XtermColor.ToString();
  264. }
  265. else
  266. {
  267. charVisColor = color.RgbColor;
  268. }
  269. }
  270. CurrentCharacterVisual = new CharacterVisual (symbol, color: color, colorCode: charVisColor);
  271. }
  272. public static Color RandomColor ()
  273. {
  274. var random = new Random ();
  275. var colorHex = random.Next (0, 0xFFFFFF).ToString ("X6");
  276. return new Color (colorHex);
  277. }
  278. public static Color AdjustColorBrightness (Color color, float brightness)
  279. {
  280. float HueToRgb (float p, float q, float t)
  281. {
  282. if (t < 0) t += 1;
  283. if (t > 1) t -= 1;
  284. if (t < 1 / 6f) return p + (q - p) * 6 * t;
  285. if (t < 1 / 2f) return q;
  286. if (t < 2 / 3f) return p + (q - p) * (2 / 3f - t) * 6;
  287. return p;
  288. }
  289. float r = int.Parse (color.RgbColor.Substring (0, 2), System.Globalization.NumberStyles.HexNumber) / 255f;
  290. float g = int.Parse (color.RgbColor.Substring (2, 2), System.Globalization.NumberStyles.HexNumber) / 255f;
  291. float b = int.Parse (color.RgbColor.Substring (4, 2), System.Globalization.NumberStyles.HexNumber) / 255f;
  292. float max = Math.Max (r, Math.Max (g, b));
  293. float min = Math.Min (r, Math.Min (g, b));
  294. float h, s, l = (max + min) / 2f;
  295. if (max == min)
  296. {
  297. h = s = 0; // achromatic
  298. }
  299. else
  300. {
  301. float d = max - min;
  302. s = l > 0.5f ? d / (2f - max - min) : d / (max + min);
  303. if (max == r)
  304. {
  305. h = (g - b) / d + (g < b ? 6 : 0);
  306. }
  307. else if (max == g)
  308. {
  309. h = (b - r) / d + 2;
  310. }
  311. else
  312. {
  313. h = (r - g) / d + 4;
  314. }
  315. h /= 6;
  316. }
  317. l = Math.Max (Math.Min (l * brightness, 1), 0);
  318. if (s == 0)
  319. {
  320. r = g = b = l; // achromatic
  321. }
  322. else
  323. {
  324. float q = l < 0.5f ? l * (1 + s) : l + s - l * s;
  325. float p = 2 * l - q;
  326. r = HueToRgb (p, q, h + 1 / 3f);
  327. g = HueToRgb (p, q, h);
  328. b = HueToRgb (p, q, h - 1 / 3f);
  329. }
  330. var adjustedColor = $"{(int)(r * 255):X2}{(int)(g * 255):X2}{(int)(b * 255):X2}";
  331. return new Color (adjustedColor);
  332. }
  333. private float EaseAnimation (EasingFunction easingFunc)
  334. {
  335. if (ActiveScene == null)
  336. {
  337. return 0;
  338. }
  339. float elapsedStepRatio = ActiveScene.EasingCurrentStep / (float)ActiveScene.EasingTotalSteps;
  340. return easingFunc (elapsedStepRatio);
  341. }
  342. public void StepAnimation ()
  343. {
  344. if (ActiveScene != null && ActiveScene.Frames.Count > 0)
  345. {
  346. if (ActiveScene.Sync != null)
  347. {
  348. if (Character.Motion.ActivePath != null)
  349. {
  350. int sequenceIndex = 0;
  351. if (ActiveScene.Sync == SyncMetric.Step)
  352. {
  353. sequenceIndex = (int)Math.Round ((ActiveScene.Frames.Count - 1) *
  354. (Math.Max (Character.Motion.ActivePath.CurrentStep, 1) /
  355. (float)Math.Max (Character.Motion.ActivePath.MaxSteps, 1)));
  356. }
  357. else if (ActiveScene.Sync == SyncMetric.Distance)
  358. {
  359. sequenceIndex = (int)Math.Round ((ActiveScene.Frames.Count - 1) *
  360. (Math.Max (Math.Max (Character.Motion.ActivePath.TotalDistance, 1) -
  361. Math.Max (Character.Motion.ActivePath.TotalDistance -
  362. Character.Motion.ActivePath.LastDistanceReached, 1), 1) /
  363. (float)Math.Max (Character.Motion.ActivePath.TotalDistance, 1)));
  364. }
  365. try
  366. {
  367. CurrentCharacterVisual = ActiveScene.Frames [sequenceIndex].CharacterVisual;
  368. }
  369. catch (IndexOutOfRangeException)
  370. {
  371. CurrentCharacterVisual = ActiveScene.Frames [^1].CharacterVisual;
  372. }
  373. }
  374. else
  375. {
  376. CurrentCharacterVisual = ActiveScene.Frames [^1].CharacterVisual;
  377. ActiveScene.PlayedFrames.AddRange (ActiveScene.Frames);
  378. ActiveScene.Frames.Clear ();
  379. }
  380. }
  381. else if (ActiveScene.Ease != null)
  382. {
  383. float easingFactor = EaseAnimation (ActiveScene.Ease);
  384. int frameIndex = (int)Math.Round (easingFactor * Math.Max (ActiveScene.EasingTotalSteps - 1, 0));
  385. frameIndex = Math.Max (Math.Min (frameIndex, ActiveScene.EasingTotalSteps - 1), 0);
  386. Frame frame = ActiveScene.FrameIndexMap [frameIndex];
  387. CurrentCharacterVisual = frame.CharacterVisual;
  388. ActiveScene.EasingCurrentStep++;
  389. if (ActiveScene.EasingCurrentStep == ActiveScene.EasingTotalSteps)
  390. {
  391. if (ActiveScene.IsLooping)
  392. {
  393. ActiveScene.EasingCurrentStep = 0;
  394. }
  395. else
  396. {
  397. ActiveScene.PlayedFrames.AddRange (ActiveScene.Frames);
  398. ActiveScene.Frames.Clear ();
  399. }
  400. }
  401. }
  402. else
  403. {
  404. CurrentCharacterVisual = ActiveScene.GetNextVisual ();
  405. }
  406. if (ActiveSceneIsComplete ())
  407. {
  408. var completedScene = ActiveScene;
  409. if (!ActiveScene.IsLooping)
  410. {
  411. ActiveScene.ResetScene ();
  412. ActiveScene = null;
  413. }
  414. Character.EventHandler.HandleEvent (Event.SceneComplete, completedScene);
  415. }
  416. }
  417. }
  418. public void ActivateScene (Scene scene)
  419. {
  420. ActiveScene = scene;
  421. ActiveSceneCurrentStep = 0;
  422. CurrentCharacterVisual = ActiveScene.Activate ();
  423. Character.EventHandler.HandleEvent (Event.SceneActivated, scene);
  424. }
  425. public void DeactivateScene (Scene scene)
  426. {
  427. if (ActiveScene == scene)
  428. {
  429. ActiveScene = null;
  430. }
  431. }
  432. }
  433. // Dummy classes for Ansitools, Colorterm, and Hexterm as placeholders
  434. public static class Ansitools
  435. {
  436. public static string ApplyBold () => "\x1b[1m";
  437. public static string ApplyItalic () => "\x1b[3m";
  438. public static string ApplyUnderline () => "\x1b[4m";
  439. public static string ApplyBlink () => "\x1b[5m";
  440. public static string ApplyReverse () => "\x1b[7m";
  441. public static string ApplyHidden () => "\x1b[8m";
  442. public static string ApplyStrikethrough () => "\x1b[9m";
  443. public static string ResetAll () => "\x1b[0m";
  444. }
  445. public static class Colorterm
  446. {
  447. public static string Fg (string colorCode) => $"\x1b[38;5;{colorCode}m";
  448. }
  449. public static class Hexterm
  450. {
  451. public static string HexToXterm (string hex)
  452. {
  453. // Convert hex color to xterm color code (0-255)
  454. return "15"; // Example output
  455. }
  456. }