Images.cs 32 KB

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