AnimationScenario.cs 9.2 KB

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