Images.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. using System.Collections.Concurrent;
  2. using System.Text;
  3. using ColorHelper;
  4. using SixLabors.ImageSharp;
  5. using SixLabors.ImageSharp.PixelFormats;
  6. using SixLabors.ImageSharp.Processing;
  7. namespace UICatalog.Scenarios;
  8. [ScenarioMetadata ("Images", "Demonstration of how to render an image with/without true color support.")]
  9. [ScenarioCategory ("Colors")]
  10. [ScenarioCategory ("Drawing")]
  11. public class Images : Scenario
  12. {
  13. private ImageView _imageView;
  14. private Point _screenLocationForSixel;
  15. private string _encodedSixelData;
  16. private Window _win;
  17. /// <summary>
  18. /// Number of sixel pixels per row of characters in the console.
  19. /// </summary>
  20. private NumericUpDown _pxY;
  21. /// <summary>
  22. /// Number of sixel pixels per column of characters in the console
  23. /// </summary>
  24. private NumericUpDown _pxX;
  25. /// <summary>
  26. /// View shown in sixel tab if sixel is supported
  27. /// </summary>
  28. private View _sixelSupported;
  29. /// <summary>
  30. /// View shown in sixel tab if sixel is not supported
  31. /// </summary>
  32. private View _sixelNotSupported;
  33. private Tab _tabSixel;
  34. private TabView _tabView;
  35. /// <summary>
  36. /// The view into which the currently opened sixel image is bounded
  37. /// </summary>
  38. private View _sixelView;
  39. private DoomFire _fire;
  40. private SixelEncoder _fireEncoder;
  41. private SixelToRender _fireSixel;
  42. private int _fireFrameCounter;
  43. private bool _isDisposed;
  44. private OptionSelector _osPaletteBuilder;
  45. private OptionSelector _osDistanceAlgorithm;
  46. private NumericUpDown _popularityThreshold;
  47. private SixelToRender _sixelImage;
  48. // Start by assuming no support
  49. private SixelSupportResult _sixelSupportResult = new ();
  50. private CheckBox _cbSupportsSixel;
  51. public override void Main ()
  52. {
  53. Application.Init ();
  54. _win = new () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" };
  55. bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false;
  56. var tabBasic = new Tab
  57. {
  58. DisplayText = "Basic"
  59. };
  60. _tabSixel = new ()
  61. {
  62. DisplayText = "Sixel"
  63. };
  64. var lblDriverName = new Label { X = 0, Y = 0, Text = $"Driver is {Application.Driver?.GetType ().Name}" };
  65. _win.Add (lblDriverName);
  66. var cbSupportsTrueColor = new CheckBox
  67. {
  68. X = Pos.Right (lblDriverName) + 2,
  69. Y = 0,
  70. CheckedState = canTrueColor ? CheckState.Checked : CheckState.UnChecked,
  71. CanFocus = false,
  72. Text = "supports true color "
  73. };
  74. _win.Add (cbSupportsTrueColor);
  75. _cbSupportsSixel = new()
  76. {
  77. X = Pos.Right (lblDriverName) + 2,
  78. Y = 1,
  79. CheckedState = CheckState.UnChecked,
  80. Text = "Supports Sixel"
  81. };
  82. var lblSupportsSixel = new Label
  83. {
  84. X = Pos.Right (lblDriverName) + 2,
  85. Y = Pos.Bottom (_cbSupportsSixel),
  86. Text = "(Check if your terminal supports Sixel)"
  87. };
  88. /* CheckedState = _sixelSupportResult.IsSupported
  89. ? CheckState.Checked
  90. : CheckState.UnChecked;*/
  91. _cbSupportsSixel.CheckedStateChanging += (s, e) =>
  92. {
  93. _sixelSupportResult.IsSupported = e.Result == CheckState.Checked;
  94. SetupSixelSupported (e.Result == CheckState.Checked);
  95. ApplyShowTabViewHack ();
  96. };
  97. _win.Add (_cbSupportsSixel);
  98. var cbUseTrueColor = new CheckBox
  99. {
  100. X = Pos.Right (cbSupportsTrueColor) + 2,
  101. Y = 0,
  102. CheckedState = !Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
  103. Enabled = canTrueColor,
  104. Text = "Use true color"
  105. };
  106. cbUseTrueColor.CheckedStateChanging += (_, evt) => Driver.Force16Colors = evt.Result == CheckState.UnChecked;
  107. _win.Add (cbUseTrueColor);
  108. var btnOpenImage = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 0, Text = "Open Image" };
  109. _win.Add (btnOpenImage);
  110. _tabView = new ()
  111. {
  112. Y = Pos.Bottom (lblSupportsSixel), Width = Dim.Fill (), Height = Dim.Fill ()
  113. };
  114. _tabView.AddTab (tabBasic, true);
  115. _tabView.AddTab (_tabSixel, false);
  116. BuildBasicTab (tabBasic);
  117. BuildSixelTab ();
  118. SetupSixelSupported (_cbSupportsSixel.CheckedState == CheckState.Checked);
  119. btnOpenImage.Accepting += OpenImage;
  120. _win.Add (lblSupportsSixel);
  121. _win.Add (_tabView);
  122. // Start trying to detect sixel support
  123. var sixelSupportDetector = new SixelSupportDetector (Application.Driver);
  124. sixelSupportDetector.Detect (UpdateSixelSupportState);
  125. Application.Run (_win);
  126. _win.Dispose ();
  127. Application.Shutdown ();
  128. }
  129. private void UpdateSixelSupportState (SixelSupportResult newResult)
  130. {
  131. _sixelSupportResult = newResult;
  132. _cbSupportsSixel.CheckedState = newResult.IsSupported ? CheckState.Checked : CheckState.UnChecked;
  133. _pxX.Value = _sixelSupportResult.Resolution.Width;
  134. _pxY.Value = _sixelSupportResult.Resolution.Height;
  135. }
  136. private void SetupSixelSupported (bool isSupported)
  137. {
  138. _tabSixel.View = isSupported ? _sixelSupported : _sixelNotSupported;
  139. _tabView.SetNeedsDraw ();
  140. }
  141. private void BtnStartFireOnAccept (object sender, CommandEventArgs e)
  142. {
  143. if (_fire != null)
  144. {
  145. return;
  146. }
  147. if (!_sixelSupportResult.SupportsTransparency)
  148. {
  149. if (MessageBox.Query (Application.Instance,
  150. "Transparency Not Supported",
  151. "It looks like your terminal does not support transparent sixel backgrounds. Do you want to try anyway?",
  152. "Yes",
  153. "No")
  154. != 0)
  155. {
  156. return;
  157. }
  158. }
  159. _fire = new (_win.Frame.Width * _pxX.Value, _win.Frame.Height * _pxY.Value);
  160. _fireEncoder = new ();
  161. _fireEncoder.Quantizer.MaxColors = Math.Min (_fireEncoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
  162. _fireEncoder.Quantizer.PaletteBuildingAlgorithm = new ConstPalette (_fire.Palette);
  163. _fireFrameCounter = 0;
  164. Application.AddTimeout (TimeSpan.FromMilliseconds (30), AdvanceFireTimerCallback);
  165. }
  166. private bool AdvanceFireTimerCallback ()
  167. {
  168. _fire.AdvanceFrame ();
  169. _fireFrameCounter++;
  170. // Control frame rate by adjusting this
  171. // Lower number means more FPS
  172. if (_fireFrameCounter % 2 != 0 || _isDisposed)
  173. {
  174. return !_isDisposed;
  175. }
  176. Color [,] bmp = _fire.GetFirePixels ();
  177. // TODO: Static way of doing this, suboptimal
  178. // ConcurrentQueue doesn't support Remove, so we update the existing object
  179. if (_fireSixel == null)
  180. {
  181. _fireSixel = new ()
  182. {
  183. SixelData = _fireEncoder.EncodeSixel (bmp),
  184. ScreenPosition = new (0, 0)
  185. };
  186. Application.GetSixels ().Enqueue (_fireSixel);
  187. }
  188. else
  189. {
  190. _fireSixel.SixelData = _fireEncoder.EncodeSixel (bmp);
  191. _fireSixel.ScreenPosition = new (0, 0);
  192. }
  193. _win.SetNeedsDraw ();
  194. return !_isDisposed;
  195. }
  196. /// <inheritdoc/>
  197. protected override void Dispose (bool disposing)
  198. {
  199. base.Dispose (disposing);
  200. _imageView.Dispose ();
  201. _sixelNotSupported.Dispose ();
  202. _sixelSupported.Dispose ();
  203. _isDisposed = true;
  204. }
  205. private void OpenImage (object sender, CommandEventArgs e)
  206. {
  207. var ofd = new OpenDialog { Title = "Open Image", AllowsMultipleSelection = false };
  208. Application.Run (ofd);
  209. if (ofd.Path is { })
  210. {
  211. Directory.SetCurrentDirectory (Path.GetFullPath (Path.GetDirectoryName (ofd.Path)!));
  212. }
  213. if (ofd.Canceled)
  214. {
  215. ofd.Dispose ();
  216. return;
  217. }
  218. string path = ofd.FilePaths [0];
  219. ofd.Dispose ();
  220. if (string.IsNullOrWhiteSpace (path))
  221. {
  222. return;
  223. }
  224. if (!File.Exists (path))
  225. {
  226. return;
  227. }
  228. Image<Rgba32> img;
  229. try
  230. {
  231. img = Image.Load<Rgba32> (File.ReadAllBytes (path));
  232. }
  233. catch (Exception ex)
  234. {
  235. MessageBox.ErrorQuery (Application.Instance, "Could not open file", ex.Message, "Ok");
  236. return;
  237. }
  238. _imageView.SetImage (img);
  239. ApplyShowTabViewHack ();
  240. Application.LayoutAndDraw ();
  241. }
  242. private void ApplyShowTabViewHack ()
  243. {
  244. // TODO HACK: This hack seems to be required to make tabview actually refresh itself
  245. _tabView.SetNeedsDraw ();
  246. Tab orig = _tabView.SelectedTab;
  247. _tabView.SelectedTab = _tabView.Tabs.Except (new [] { orig }).ElementAt (0);
  248. _tabView.SelectedTab = orig;
  249. }
  250. private void BuildBasicTab (Tab tabBasic)
  251. {
  252. _imageView = new ()
  253. {
  254. Width = Dim.Fill (),
  255. Height = Dim.Fill (),
  256. CanFocus = true
  257. };
  258. tabBasic.View = _imageView;
  259. }
  260. private void BuildSixelTab ()
  261. {
  262. _sixelSupported = new ()
  263. {
  264. Width = Dim.Fill (),
  265. Height = Dim.Fill (),
  266. CanFocus = true
  267. };
  268. _sixelNotSupported = new ()
  269. {
  270. Width = Dim.Fill (),
  271. Height = Dim.Fill (),
  272. CanFocus = true
  273. };
  274. _sixelNotSupported.Add (
  275. new Label
  276. {
  277. Width = Dim.Fill (),
  278. Height = Dim.Fill (),
  279. TextAlignment = Alignment.Center,
  280. Text = "Your driver does not support Sixel image format",
  281. VerticalTextAlignment = Alignment.Center
  282. });
  283. _sixelView = new ()
  284. {
  285. Width = Dim.Percent (50),
  286. Height = Dim.Fill (),
  287. BorderStyle = LineStyle.Dotted
  288. };
  289. _sixelSupported.Add (_sixelView);
  290. var btnSixel = new Button
  291. {
  292. X = Pos.Right (_sixelView),
  293. Y = 0,
  294. Text = "Output Sixel", Width = Dim.Auto ()
  295. };
  296. btnSixel.Accepting += OutputSixelButtonClick;
  297. _sixelSupported.Add (btnSixel);
  298. var btnStartFire = new Button
  299. {
  300. X = Pos.Right (_sixelView),
  301. Y = Pos.Bottom (btnSixel),
  302. Text = "Start Fire"
  303. };
  304. btnStartFire.Accepting += BtnStartFireOnAccept;
  305. _sixelSupported.Add (btnStartFire);
  306. var lblPxX = new Label
  307. {
  308. X = Pos.Right (_sixelView),
  309. Y = Pos.Bottom (btnStartFire) + 1,
  310. Text = "Pixels per Col:"
  311. };
  312. _pxX = new ()
  313. {
  314. X = Pos.Right (lblPxX),
  315. Y = Pos.Bottom (btnStartFire) + 1,
  316. Value = _sixelSupportResult.Resolution.Width
  317. };
  318. var lblPxY = new Label
  319. {
  320. X = lblPxX.X,
  321. Y = Pos.Bottom (_pxX),
  322. Text = "Pixels per Row:"
  323. };
  324. _pxY = new ()
  325. {
  326. X = Pos.Right (lblPxY),
  327. Y = Pos.Bottom (_pxX),
  328. Value = _sixelSupportResult.Resolution.Height
  329. };
  330. var l1 = new Label
  331. {
  332. Text = "Palette Building Algorithm",
  333. Width = Dim.Auto (),
  334. X = Pos.Right (_sixelView),
  335. Y = Pos.Bottom (_pxY) + 1
  336. };
  337. _osPaletteBuilder = new ()
  338. {
  339. Labels =
  340. [
  341. "Popularity",
  342. "Median Cut"
  343. ],
  344. X = Pos.Right (_sixelView) + 2,
  345. Y = Pos.Bottom (l1),
  346. Value = 1
  347. };
  348. _popularityThreshold = new ()
  349. {
  350. X = Pos.Right (_osPaletteBuilder) + 1,
  351. Y = Pos.Top (_osPaletteBuilder),
  352. Value = 8
  353. };
  354. var lblPopThreshold = new Label
  355. {
  356. Text = "(threshold)",
  357. X = Pos.Right (_popularityThreshold),
  358. Y = Pos.Top (_popularityThreshold)
  359. };
  360. var l2 = new Label
  361. {
  362. Text = "Color Distance Algorithm",
  363. Width = Dim.Auto (),
  364. X = Pos.Right (_sixelView),
  365. Y = Pos.Bottom (_osPaletteBuilder) + 1
  366. };
  367. _osDistanceAlgorithm = new ()
  368. {
  369. Labels = new []
  370. {
  371. "Euclidian",
  372. "CIE76"
  373. },
  374. X = Pos.Right (_sixelView) + 2,
  375. Y = Pos.Bottom (l2)
  376. };
  377. _sixelSupported.Add (lblPxX);
  378. _sixelSupported.Add (_pxX);
  379. _sixelSupported.Add (lblPxY);
  380. _sixelSupported.Add (_pxY);
  381. _sixelSupported.Add (l1);
  382. _sixelSupported.Add (_osPaletteBuilder);
  383. _sixelSupported.Add (l2);
  384. _sixelSupported.Add (_osDistanceAlgorithm);
  385. _sixelSupported.Add (_popularityThreshold);
  386. _sixelSupported.Add (lblPopThreshold);
  387. _sixelView.DrawingContent += SixelViewOnDrawingContent;
  388. }
  389. private IPaletteBuilder GetPaletteBuilder ()
  390. {
  391. switch (_osPaletteBuilder.Value)
  392. {
  393. case 0: return new PopularityPaletteWithThreshold (GetDistanceAlgorithm (), _popularityThreshold.Value);
  394. case 1: return new MedianCutPaletteBuilder (GetDistanceAlgorithm ());
  395. default: throw new ArgumentOutOfRangeException ();
  396. }
  397. }
  398. private IColorDistance GetDistanceAlgorithm ()
  399. {
  400. switch (_osDistanceAlgorithm.Value)
  401. {
  402. case 0: return new EuclideanColorDistance ();
  403. case 1: return new CIE76ColorDistance ();
  404. default: throw new ArgumentOutOfRangeException ();
  405. }
  406. }
  407. private void OutputSixelButtonClick (object sender, CommandEventArgs e)
  408. {
  409. if (_imageView.FullResImage == null)
  410. {
  411. MessageBox.Query (Application.Instance, "No Image Loaded", "You must first open an image. Use the 'Open Image' button above.", "Ok");
  412. return;
  413. }
  414. _screenLocationForSixel = _sixelView.FrameToScreen ().Location;
  415. _encodedSixelData = GenerateSixelData (
  416. _imageView.FullResImage,
  417. _sixelView.Frame.Size,
  418. _pxX.Value,
  419. _pxY.Value);
  420. if (_sixelImage == null)
  421. {
  422. _sixelImage = new ()
  423. {
  424. SixelData = _encodedSixelData,
  425. ScreenPosition = _screenLocationForSixel
  426. };
  427. Application.GetSixels ().Enqueue (_sixelImage);
  428. }
  429. else
  430. {
  431. _sixelImage.ScreenPosition = _screenLocationForSixel;
  432. _sixelImage.SixelData = _encodedSixelData;
  433. }
  434. _sixelView.SetNeedsDraw ();
  435. }
  436. private void SixelViewOnDrawingContent (object sender, DrawEventArgs e)
  437. {
  438. if (!string.IsNullOrWhiteSpace (_encodedSixelData))
  439. {
  440. // Does not work (see https://github.com/gui-cs/Terminal.Gui/issues/3763)
  441. // Application.Driver?.Move (_screenLocationForSixel.X, _screenLocationForSixel.Y);
  442. // Application.Driver?.AddStr (_encodedSixelData);
  443. // Works in DotNetDriver but results in screen flicker when moving mouse but vanish instantly
  444. // Console.SetCursorPosition (_screenLocationForSixel.X, _screenLocationForSixel.Y);
  445. // Console.Write (_encodedSixelData);
  446. }
  447. }
  448. public string GenerateSixelData (
  449. Image<Rgba32> fullResImage,
  450. Size maxSize,
  451. int pixelsPerCellX,
  452. int pixelsPerCellY
  453. )
  454. {
  455. var encoder = new SixelEncoder ();
  456. encoder.Quantizer.MaxColors = Math.Min (encoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
  457. encoder.Quantizer.PaletteBuildingAlgorithm = GetPaletteBuilder ();
  458. encoder.Quantizer.DistanceAlgorithm = GetDistanceAlgorithm ();
  459. // Calculate the target size in pixels based on console units
  460. int targetWidthInPixels = maxSize.Width * pixelsPerCellX;
  461. int targetHeightInPixels = maxSize.Height * pixelsPerCellY;
  462. // Get the original image dimensions
  463. int originalWidth = fullResImage.Width;
  464. int originalHeight = fullResImage.Height;
  465. // Use the helper function to get the resized dimensions while maintaining the aspect ratio
  466. Size newSize = CalculateAspectRatioFit (originalWidth, originalHeight, targetWidthInPixels, targetHeightInPixels);
  467. // Resize the image to match the console size
  468. Image<Rgba32> resizedImage = fullResImage.Clone (x => x.Resize (newSize.Width, newSize.Height));
  469. string encoded = encoder.EncodeSixel (ConvertToColorArray (resizedImage));
  470. var pv = new PaletteView (encoder.Quantizer.Palette.ToList ());
  471. var dlg = new Dialog
  472. {
  473. Title = "Palette (Esc to close)",
  474. Width = Dim.Fill (2),
  475. Height = Dim.Fill (1)
  476. };
  477. var btn = new Button
  478. {
  479. Text = "Ok"
  480. };
  481. btn.Accepting += (s, e) => Application.RequestStop ();
  482. dlg.Add (pv);
  483. dlg.AddButton (btn);
  484. Application.Run (dlg);
  485. dlg.Dispose ();
  486. return encoded;
  487. }
  488. private Size CalculateAspectRatioFit (int originalWidth, int originalHeight, int targetWidth, int targetHeight)
  489. {
  490. // Calculate the scaling factor for width and height
  491. double widthScale = (double)targetWidth / originalWidth;
  492. double heightScale = (double)targetHeight / originalHeight;
  493. // Use the smaller scaling factor to maintain the aspect ratio
  494. double scale = Math.Min (widthScale, heightScale);
  495. // Calculate the new width and height while keeping the aspect ratio
  496. var newWidth = (int)(originalWidth * scale);
  497. var newHeight = (int)(originalHeight * scale);
  498. // Return the new size as a Size object
  499. return new (newWidth, newHeight);
  500. }
  501. public static Color [,] ConvertToColorArray (Image<Rgba32> image)
  502. {
  503. int width = image.Width;
  504. int height = image.Height;
  505. Color [,] colors = new Color [width, height];
  506. // Loop through each pixel and convert Rgba32 to Terminal.Gui color
  507. for (var x = 0; x < width; x++)
  508. {
  509. for (var y = 0; y < height; y++)
  510. {
  511. Rgba32 pixel = image [x, y];
  512. colors [x, y] = new (pixel.R, pixel.G, pixel.B); // Convert Rgba32 to Terminal.Gui color
  513. }
  514. }
  515. return colors;
  516. }
  517. private class ImageView : View
  518. {
  519. private readonly ConcurrentDictionary<Rgba32, Attribute> _cache = new ();
  520. public Image<Rgba32> FullResImage;
  521. private Image<Rgba32> _matchSize;
  522. protected override bool OnDrawingContent (DrawContext context)
  523. {
  524. if (FullResImage == null)
  525. {
  526. return true;
  527. }
  528. // if we have not got a cached resized image of this size
  529. if (_matchSize == null || Viewport.Width != _matchSize.Width || Viewport.Height != _matchSize.Height)
  530. {
  531. // generate one
  532. _matchSize = FullResImage.Clone (x => x.Resize (Viewport.Width, Viewport.Height));
  533. }
  534. for (var y = 0; y < Viewport.Height; y++)
  535. {
  536. for (var x = 0; x < Viewport.Width; x++)
  537. {
  538. Rgba32 rgb = _matchSize [x, y];
  539. Attribute attr = _cache.GetOrAdd (
  540. rgb,
  541. rgb => new (
  542. new Color (),
  543. new Color (rgb.R, rgb.G, rgb.B)
  544. )
  545. );
  546. SetAttribute (attr);
  547. AddRune (x, y, (Rune)' ');
  548. }
  549. }
  550. return true;
  551. }
  552. internal void SetImage (Image<Rgba32> image)
  553. {
  554. FullResImage = image;
  555. SetNeedsDraw ();
  556. }
  557. }
  558. public class PaletteView : View
  559. {
  560. private readonly List<Color> _palette;
  561. public PaletteView (List<Color> palette)
  562. {
  563. _palette = palette ?? new List<Color> ();
  564. Width = Dim.Fill ();
  565. Height = Dim.Fill ();
  566. }
  567. // Automatically calculates rows and columns based on the available bounds
  568. private (int columns, int rows) CalculateGridSize (Rectangle bounds)
  569. {
  570. // Characters are twice as wide as they are tall, so use 2:1 width-to-height ratio
  571. int availableWidth = Viewport.Width / 2; // Each color block is 2 character wide
  572. int availableHeight = Viewport.Height;
  573. int numColors = _palette.Count;
  574. // Calculate the number of columns and rows we can fit within the bounds
  575. int columns = Math.Min (availableWidth, numColors);
  576. int rows = (numColors + columns - 1) / columns; // Ceiling division for rows
  577. // Ensure we do not exceed the available height
  578. if (rows > availableHeight)
  579. {
  580. rows = availableHeight;
  581. columns = (numColors + rows - 1) / rows; // Recalculate columns if needed
  582. }
  583. return (columns, rows);
  584. }
  585. protected override bool OnDrawingContent (DrawContext context)
  586. {
  587. if (_palette == null || _palette.Count == 0)
  588. {
  589. return false;
  590. }
  591. // Calculate the grid size based on the bounds
  592. (int columns, int rows) = CalculateGridSize (Viewport);
  593. // Draw the colors in the palette
  594. for (var i = 0; i < _palette.Count && i < columns * rows; i++)
  595. {
  596. int row = i / columns;
  597. int col = i % columns;
  598. // Calculate position in the grid
  599. int x = col * 2; // Each color block takes up 2 horizontal spaces
  600. int y = row;
  601. // Set the color attribute for the block
  602. SetAttribute (new (_palette [i], _palette [i]));
  603. // Draw the block (2 characters wide per block)
  604. for (var dx = 0; dx < 2; dx++) // Fill the width of the block
  605. {
  606. AddRune (x + dx, y, (Rune)' ');
  607. }
  608. }
  609. return true;
  610. }
  611. }
  612. }
  613. internal class ConstPalette : IPaletteBuilder
  614. {
  615. private readonly List<Color> _palette;
  616. public ConstPalette (Color [] palette) { _palette = palette.ToList (); }
  617. /// <inheritdoc/>
  618. public List<Color> BuildPalette (List<Color> colors, int maxColors) { return _palette; }
  619. }
  620. public abstract class LabColorDistance : IColorDistance
  621. {
  622. // Reference white point for D65 illuminant (can be moved to constants)
  623. private const double RefX = 95.047;
  624. private const double RefY = 100.000;
  625. private const double RefZ = 108.883;
  626. // Conversion from RGB to Lab
  627. protected LabColor RgbToLab (Color c)
  628. {
  629. XYZ xyz = ColorConverter.RgbToXyz (new (c.R, c.G, c.B));
  630. // Normalize XYZ values by reference white point
  631. double x = xyz.X / RefX;
  632. double y = xyz.Y / RefY;
  633. double z = xyz.Z / RefZ;
  634. // Apply the nonlinear transformation for Lab
  635. x = x > 0.008856 ? Math.Pow (x, 1.0 / 3.0) : 7.787 * x + 16.0 / 116.0;
  636. y = y > 0.008856 ? Math.Pow (y, 1.0 / 3.0) : 7.787 * y + 16.0 / 116.0;
  637. z = z > 0.008856 ? Math.Pow (z, 1.0 / 3.0) : 7.787 * z + 16.0 / 116.0;
  638. // Calculate Lab values
  639. double l = 116.0 * y - 16.0;
  640. double a = 500.0 * (x - y);
  641. double b = 200.0 * (y - z);
  642. return new (l, a, b);
  643. }
  644. // LabColor class encapsulating L, A, and B values
  645. protected class LabColor
  646. {
  647. public double L { get; }
  648. public double A { get; }
  649. public double B { get; }
  650. public LabColor (double l, double a, double b)
  651. {
  652. L = l;
  653. A = a;
  654. B = b;
  655. }
  656. }
  657. /// <inheritdoc/>
  658. public abstract double CalculateDistance (Color c1, Color c2);
  659. }
  660. /// <summary>
  661. /// This is the simplest method to measure color difference in the CIE Lab color space. The Euclidean distance in Lab
  662. /// space is more aligned with human perception than RGB space, as Lab attempts to model how humans perceive color
  663. /// differences.
  664. /// </summary>
  665. public class CIE76ColorDistance : LabColorDistance
  666. {
  667. public override double CalculateDistance (Color c1, Color c2)
  668. {
  669. LabColor lab1 = RgbToLab (c1);
  670. LabColor lab2 = RgbToLab (c2);
  671. // Euclidean distance in Lab color space
  672. return Math.Sqrt (Math.Pow (lab1.L - lab2.L, 2) + Math.Pow (lab1.A - lab2.A, 2) + Math.Pow (lab1.B - lab2.B, 2));
  673. }
  674. }
  675. public class MedianCutPaletteBuilder : IPaletteBuilder
  676. {
  677. private readonly IColorDistance _colorDistance;
  678. public MedianCutPaletteBuilder (IColorDistance colorDistance) { _colorDistance = colorDistance; }
  679. public List<Color> BuildPalette (List<Color> colors, int maxColors)
  680. {
  681. if (colors == null || colors.Count == 0 || maxColors <= 0)
  682. {
  683. return new ();
  684. }
  685. return MedianCut (colors, maxColors);
  686. }
  687. private List<Color> MedianCut (List<Color> colors, int maxColors)
  688. {
  689. List<List<Color>> cubes = new () { colors };
  690. // Recursively split color regions
  691. while (cubes.Count < maxColors)
  692. {
  693. var added = false;
  694. cubes.Sort ((a, b) => Volume (a).CompareTo (Volume (b)));
  695. List<Color> largestCube = cubes.Last ();
  696. cubes.RemoveAt (cubes.Count - 1);
  697. // Check if the largest cube contains only one unique color
  698. if (IsSingleColorCube (largestCube))
  699. {
  700. // Add back and stop splitting this cube
  701. cubes.Add (largestCube);
  702. break;
  703. }
  704. (List<Color> cube1, List<Color> cube2) = SplitCube (largestCube);
  705. if (cube1.Any ())
  706. {
  707. cubes.Add (cube1);
  708. added = true;
  709. }
  710. if (cube2.Any ())
  711. {
  712. cubes.Add (cube2);
  713. added = true;
  714. }
  715. // Break the loop if no new cubes were added
  716. if (!added)
  717. {
  718. break;
  719. }
  720. }
  721. // Calculate average color for each cube
  722. return cubes.Select (AverageColor).Distinct ().ToList ();
  723. }
  724. // Checks if all colors in the cube are the same
  725. private bool IsSingleColorCube (List<Color> cube)
  726. {
  727. Color firstColor = cube.First ();
  728. return cube.All (c => c.R == firstColor.R && c.G == firstColor.G && c.B == firstColor.B);
  729. }
  730. // Splits the cube based on the largest color component range
  731. private (List<Color>, List<Color>) SplitCube (List<Color> cube)
  732. {
  733. (int component, int range) = FindLargestRange (cube);
  734. // Sort by the largest color range component (either R, G, or B)
  735. cube.Sort (
  736. (c1, c2) => component switch
  737. {
  738. 0 => c1.R.CompareTo (c2.R),
  739. 1 => c1.G.CompareTo (c2.G),
  740. 2 => c1.B.CompareTo (c2.B),
  741. _ => 0
  742. });
  743. int medianIndex = cube.Count / 2;
  744. List<Color> cube1 = cube.Take (medianIndex).ToList ();
  745. List<Color> cube2 = cube.Skip (medianIndex).ToList ();
  746. return (cube1, cube2);
  747. }
  748. private (int, int) FindLargestRange (List<Color> cube)
  749. {
  750. byte minR = cube.Min (c => c.R);
  751. byte maxR = cube.Max (c => c.R);
  752. byte minG = cube.Min (c => c.G);
  753. byte maxG = cube.Max (c => c.G);
  754. byte minB = cube.Min (c => c.B);
  755. byte maxB = cube.Max (c => c.B);
  756. int rangeR = maxR - minR;
  757. int rangeG = maxG - minG;
  758. int rangeB = maxB - minB;
  759. if (rangeR >= rangeG && rangeR >= rangeB)
  760. {
  761. return (0, rangeR);
  762. }
  763. if (rangeG >= rangeR && rangeG >= rangeB)
  764. {
  765. return (1, rangeG);
  766. }
  767. return (2, rangeB);
  768. }
  769. private Color AverageColor (List<Color> cube)
  770. {
  771. var avgR = (byte)cube.Average (c => c.R);
  772. var avgG = (byte)cube.Average (c => c.G);
  773. var avgB = (byte)cube.Average (c => c.B);
  774. return new (avgR, avgG, avgB);
  775. }
  776. private int Volume (List<Color> cube)
  777. {
  778. if (cube == null || cube.Count == 0)
  779. {
  780. // Return a volume of 0 if the cube is empty or null
  781. return 0;
  782. }
  783. byte minR = cube.Min (c => c.R);
  784. byte maxR = cube.Max (c => c.R);
  785. byte minG = cube.Min (c => c.G);
  786. byte maxG = cube.Max (c => c.G);
  787. byte minB = cube.Min (c => c.B);
  788. byte maxB = cube.Max (c => c.B);
  789. return (maxR - minR) * (maxG - minG) * (maxB - minB);
  790. }
  791. }
  792. public class DoomFire
  793. {
  794. private readonly int _width;
  795. private readonly int _height;
  796. private readonly Color [,] _firePixels;
  797. private static Color [] _palette;
  798. public Color [] Palette => _palette;
  799. private readonly Random _random = new ();
  800. public DoomFire (int width, int height)
  801. {
  802. _width = width;
  803. _height = height;
  804. _firePixels = new Color [width, height];
  805. InitializePalette ();
  806. InitializeFire ();
  807. }
  808. private void InitializePalette ()
  809. {
  810. // Initialize a basic fire palette. You can modify these colors as needed.
  811. _palette = new Color [37]; // Using 37 colors as per the original Doom fire palette scale.
  812. // First color is transparent black
  813. _palette [0] = new (0, 0, 0, 0); // Transparent black (ARGB)
  814. // The rest of the palette is fire colors
  815. for (var i = 1; i < 37; i++)
  816. {
  817. var r = (byte)Math.Min (255, i * 7);
  818. var g = (byte)Math.Min (255, i * 5);
  819. var b = (byte)Math.Min (255, i * 2);
  820. _palette [i] = new (r, g, b); // Full opacity
  821. }
  822. }
  823. public void InitializeFire ()
  824. {
  825. // Set the bottom row to full intensity (simulate the base of the fire).
  826. for (var x = 0; x < _width; x++)
  827. {
  828. _firePixels [x, _height - 1] = _palette [36]; // Max intensity fire.
  829. }
  830. // Set the rest of the pixels to black (transparent).
  831. for (var y = 0; y < _height - 1; y++)
  832. {
  833. for (var x = 0; x < _width; x++)
  834. {
  835. _firePixels [x, y] = _palette [0]; // Transparent black
  836. }
  837. }
  838. }
  839. public void AdvanceFrame ()
  840. {
  841. // Process every pixel except the bottom row
  842. for (var x = 0; x < _width; x++)
  843. {
  844. for (var y = 1; y < _height; y++) // Skip the last row (which is always max intensity)
  845. {
  846. int srcX = x;
  847. int srcY = y;
  848. int dstY = y - 1;
  849. // Spread fire upwards with randomness
  850. int decay = _random.Next (0, 2);
  851. int dstX = srcX + _random.Next (-1, 2);
  852. if (dstX < 0 || dstX >= _width) // Prevent out of bounds
  853. {
  854. dstX = srcX;
  855. }
  856. // Get the fire color from below and reduce its intensity
  857. Color srcColor = _firePixels [srcX, srcY];
  858. int intensity = Array.IndexOf (_palette, srcColor) - decay;
  859. if (intensity < 0)
  860. {
  861. intensity = 0;
  862. }
  863. _firePixels [dstX, dstY] = _palette [intensity];
  864. }
  865. }
  866. }
  867. public Color [,] GetFirePixels () { return _firePixels; }
  868. }