Animation.cs 16 KB

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