Images.cs 32 KB

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