AnimationScenario.cs 9.4 KB

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