AnimationScenario.cs 9.0 KB

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