AnimationScenario.cs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. #nullable enable
  2. using System;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Reflection;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using SixLabors.ImageSharp;
  9. using SixLabors.ImageSharp.PixelFormats;
  10. using SixLabors.ImageSharp.Processing;
  11. using Terminal.Gui;
  12. namespace UICatalog.Scenarios;
  13. [ScenarioMetadata ("Animation", "Demonstration of how to render animated images with threading.")]
  14. [ScenarioCategory ("Threading")]
  15. [ScenarioCategory ("Drawing")]
  16. public class AnimationScenario : Scenario
  17. {
  18. private ImageView? _imageView;
  19. public override void Main ()
  20. {
  21. Application.Init ();
  22. var win = new Window
  23. {
  24. Title = GetQuitKeyAndName (),
  25. X = 0,
  26. Y = 0,
  27. Width = Dim.Fill (),
  28. Height = Dim.Fill (),
  29. };
  30. _imageView = new ImageView { Width = Dim.Fill (), Height = Dim.Fill ()! - 2 };
  31. win.Add (_imageView);
  32. var lbl = new Label { Y = Pos.AnchorEnd (), Text = "Image by Wikiscient" };
  33. win.Add (lbl);
  34. var lbl2 = new Label
  35. {
  36. X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Text = "https://commons.wikimedia.org/wiki/File:Spinning_globe.gif"
  37. };
  38. win.Add (lbl2);
  39. // Start the animation after the window is initialized
  40. win.Initialized += OnWinOnInitialized;
  41. Application.Run (win);
  42. win.Dispose ();
  43. Application.Shutdown ();
  44. }
  45. private void OnWinOnInitialized (object? sender, EventArgs args)
  46. {
  47. DirectoryInfo dir;
  48. string assemblyLocation = Assembly.GetExecutingAssembly ().Location;
  49. if (!string.IsNullOrEmpty (assemblyLocation))
  50. {
  51. dir = new DirectoryInfo (Path.GetDirectoryName (assemblyLocation) ?? string.Empty);
  52. }
  53. else
  54. {
  55. dir = new DirectoryInfo (AppContext.BaseDirectory);
  56. }
  57. var f = new FileInfo (
  58. Path.Combine (dir.FullName, "Scenarios/AnimationScenario", "Spinning_globe_dark_small.gif")
  59. );
  60. if (!f.Exists)
  61. {
  62. Debug.WriteLine ($"Could not find {f.FullName}");
  63. MessageBox.ErrorQuery ("Could not find gif", $"Could not find\n{f.FullName}", "Ok");
  64. return;
  65. }
  66. _imageView!.SetImage (Image.Load<Rgba32> (File.ReadAllBytes (f.FullName)));
  67. Task.Run (
  68. () =>
  69. {
  70. while (Application.Initialized)
  71. {
  72. // When updating from a Thread/Task always use Invoke
  73. Application.Invoke (
  74. () =>
  75. {
  76. _imageView.NextFrame ();
  77. _imageView.SetNeedsDraw ();
  78. });
  79. Task.Delay (100).Wait ();
  80. }
  81. });
  82. }
  83. // This is a C# port of https://github.com/andraaspar/bitmap-to-braille by Andraaspar
  84. /// <summary>Renders an image as unicode Braille.</summary>
  85. public class BitmapToBraille (int widthPixels, int heightPixels, Func<int, int, bool> pixelIsLit)
  86. {
  87. public const int CHAR_HEIGHT = 4;
  88. public const int CHAR_WIDTH = 2;
  89. private const string CHARS =
  90. " ⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙⡚⡛⡜⡝⡞⡟⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷⠸⠹⠺⠻⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕⣖⣗⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯⢰⢱⢲⢳⢴⢵⢶⢷⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿";
  91. public int HeightPixels { get; } = heightPixels;
  92. public Func<int, int, bool> PixelIsLit { get; } = pixelIsLit;
  93. public int WidthPixels { get; } = widthPixels;
  94. public string GenerateImage ()
  95. {
  96. var imageHeightChars = (int)Math.Ceiling ((double)HeightPixels / CHAR_HEIGHT);
  97. var imageWidthChars = (int)Math.Ceiling ((double)WidthPixels / CHAR_WIDTH);
  98. var result = new StringBuilder ();
  99. for (var y = 0; y < imageHeightChars; y++)
  100. {
  101. for (var x = 0; x < imageWidthChars; x++)
  102. {
  103. int baseX = x * CHAR_WIDTH;
  104. int baseY = y * CHAR_HEIGHT;
  105. var charIndex = 0;
  106. var value = 1;
  107. for (var charX = 0; charX < CHAR_WIDTH; charX++)
  108. {
  109. for (var charY = 0; charY < CHAR_HEIGHT; charY++)
  110. {
  111. int bitmapX = baseX + charX;
  112. int bitmapY = baseY + charY;
  113. bool pixelExists = bitmapX < WidthPixels && bitmapY < HeightPixels;
  114. if (pixelExists && PixelIsLit (bitmapX, bitmapY))
  115. {
  116. charIndex += value;
  117. }
  118. value *= 2;
  119. }
  120. }
  121. result.Append (CHARS [charIndex]);
  122. }
  123. result.Append ('\n');
  124. }
  125. return result.ToString ().TrimEnd ();
  126. }
  127. }
  128. private class ImageView : View
  129. {
  130. private string []? _brailleCache;
  131. private int _currentFrame;
  132. private int _frameCount;
  133. private Image<Rgba32> []? _fullResImages;
  134. private Image<Rgba32> []? _matchSizes;
  135. private Rectangle _oldSize = Rectangle.Empty;
  136. public void NextFrame () { _currentFrame = (_currentFrame + 1) % _frameCount; }
  137. protected override bool OnDrawingContent ()
  138. {
  139. if (_frameCount == 0)
  140. {
  141. return false;
  142. }
  143. if (_oldSize != Viewport)
  144. {
  145. // Invalidate cached images now size has changed
  146. _matchSizes = new Image<Rgba32> [_frameCount];
  147. _brailleCache = new string [_frameCount];
  148. _oldSize = Viewport;
  149. }
  150. Image<Rgba32>? imgScaled = _matchSizes? [_currentFrame];
  151. string? braille = _brailleCache? [_currentFrame];
  152. if (imgScaled == null)
  153. {
  154. Image<Rgba32>? imgFull = _fullResImages? [_currentFrame];
  155. // keep aspect ratio
  156. int newSize = Math.Min (Viewport.Width, Viewport.Height);
  157. // generate one
  158. if (_matchSizes is { } && imgFull is { })
  159. {
  160. _matchSizes [_currentFrame] = imgScaled = imgFull.Clone (
  161. x => x.Resize (
  162. newSize * BitmapToBraille.CHAR_HEIGHT,
  163. newSize * BitmapToBraille.CHAR_HEIGHT
  164. )
  165. );
  166. }
  167. }
  168. if (braille == null && _brailleCache is { })
  169. {
  170. _brailleCache [_currentFrame] = braille = GetBraille (_matchSizes? [_currentFrame]!);
  171. }
  172. string []? lines = braille?.Split ('\n');
  173. for (var y = 0; y < lines!.Length; y++)
  174. {
  175. string line = lines [y];
  176. for (var x = 0; x < line.Length; x++)
  177. {
  178. AddRune (x, y, (Rune)line [x]);
  179. }
  180. }
  181. return true;
  182. }
  183. internal void SetImage (Image<Rgba32> image)
  184. {
  185. _frameCount = image.Frames.Count;
  186. _fullResImages = new Image<Rgba32> [_frameCount];
  187. _matchSizes = new Image<Rgba32> [_frameCount];
  188. _brailleCache = new string [_frameCount];
  189. for (var i = 0; i < _frameCount - 1; i++)
  190. {
  191. _fullResImages [i] = image.Frames.ExportFrame (0);
  192. }
  193. _fullResImages [_frameCount - 1] = image;
  194. SetNeedsDraw ();
  195. }
  196. private string GetBraille (Image<Rgba32> img)
  197. {
  198. var braille = new BitmapToBraille (
  199. img.Width,
  200. img.Height,
  201. (x, y) => IsLit (img, x, y)
  202. );
  203. return braille.GenerateImage ();
  204. }
  205. private bool IsLit (Image<Rgba32> img, int x, int y)
  206. {
  207. Rgba32 rgb = img [x, y];
  208. return rgb.R + rgb.G + rgb.B > 50;
  209. }
  210. }
  211. }