| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059 |
- using System.Collections.Concurrent;
- using System.Text;
- using ColorHelper;
- using SixLabors.ImageSharp;
- using SixLabors.ImageSharp.PixelFormats;
- using SixLabors.ImageSharp.Processing;
- namespace UICatalog.Scenarios;
- [ScenarioMetadata ("Images", "Demonstration of how to render an image with/without true color support.")]
- [ScenarioCategory ("Colors")]
- [ScenarioCategory ("Drawing")]
- public class Images : Scenario
- {
- private ImageView _imageView;
- private Point _screenLocationForSixel;
- private string _encodedSixelData;
- private Window _win;
- /// <summary>
- /// Number of sixel pixels per row of characters in the console.
- /// </summary>
- private NumericUpDown _pxY;
- /// <summary>
- /// Number of sixel pixels per column of characters in the console
- /// </summary>
- private NumericUpDown _pxX;
- /// <summary>
- /// View shown in sixel tab if sixel is supported
- /// </summary>
- private View _sixelSupported;
- /// <summary>
- /// View shown in sixel tab if sixel is not supported
- /// </summary>
- private View _sixelNotSupported;
- private Tab _tabSixel;
- private TabView _tabView;
- /// <summary>
- /// The view into which the currently opened sixel image is bounded
- /// </summary>
- private View _sixelView;
- private DoomFire _fire;
- private SixelEncoder _fireEncoder;
- private SixelToRender _fireSixel;
- private int _fireFrameCounter;
- private bool _isDisposed;
- private OptionSelector _osPaletteBuilder;
- private OptionSelector _osDistanceAlgorithm;
- private NumericUpDown _popularityThreshold;
- private SixelToRender _sixelImage;
- // Start by assuming no support
- private SixelSupportResult _sixelSupportResult = new ();
- private CheckBox _cbSupportsSixel;
- public override void Main ()
- {
- Application.Init ();
- _win = new () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" };
- bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false;
- var tabBasic = new Tab
- {
- DisplayText = "Basic"
- };
- _tabSixel = new ()
- {
- DisplayText = "Sixel"
- };
- var lblDriverName = new Label { X = 0, Y = 0, Text = $"Driver is {Application.Driver?.GetType ().Name}" };
- _win.Add (lblDriverName);
- var cbSupportsTrueColor = new CheckBox
- {
- X = Pos.Right (lblDriverName) + 2,
- Y = 0,
- CheckedState = canTrueColor ? CheckState.Checked : CheckState.UnChecked,
- CanFocus = false,
- Text = "supports true color "
- };
- _win.Add (cbSupportsTrueColor);
- _cbSupportsSixel = new()
- {
- X = Pos.Right (lblDriverName) + 2,
- Y = 1,
- CheckedState = CheckState.UnChecked,
- Text = "Supports Sixel"
- };
- var lblSupportsSixel = new Label
- {
- X = Pos.Right (lblDriverName) + 2,
- Y = Pos.Bottom (_cbSupportsSixel),
- Text = "(Check if your terminal supports Sixel)"
- };
- /* CheckedState = _sixelSupportResult.IsSupported
- ? CheckState.Checked
- : CheckState.UnChecked;*/
- _cbSupportsSixel.CheckedStateChanging += (s, e) =>
- {
- _sixelSupportResult.IsSupported = e.Result == CheckState.Checked;
- SetupSixelSupported (e.Result == CheckState.Checked);
- ApplyShowTabViewHack ();
- };
- _win.Add (_cbSupportsSixel);
- var cbUseTrueColor = new CheckBox
- {
- X = Pos.Right (cbSupportsTrueColor) + 2,
- Y = 0,
- CheckedState = !Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
- Enabled = canTrueColor,
- Text = "Use true color"
- };
- cbUseTrueColor.CheckedStateChanging += (_, evt) => Driver.Force16Colors = evt.Result == CheckState.UnChecked;
- _win.Add (cbUseTrueColor);
- var btnOpenImage = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 0, Text = "Open Image" };
- _win.Add (btnOpenImage);
- _tabView = new ()
- {
- Y = Pos.Bottom (lblSupportsSixel), Width = Dim.Fill (), Height = Dim.Fill ()
- };
- _tabView.AddTab (tabBasic, true);
- _tabView.AddTab (_tabSixel, false);
- BuildBasicTab (tabBasic);
- BuildSixelTab ();
- SetupSixelSupported (_cbSupportsSixel.CheckedState == CheckState.Checked);
- btnOpenImage.Accepting += OpenImage;
- _win.Add (lblSupportsSixel);
- _win.Add (_tabView);
- // Start trying to detect sixel support
- var sixelSupportDetector = new SixelSupportDetector (Application.Driver);
- sixelSupportDetector.Detect (UpdateSixelSupportState);
- Application.Run (_win);
- _win.Dispose ();
- Application.Shutdown ();
- }
- private void UpdateSixelSupportState (SixelSupportResult newResult)
- {
- _sixelSupportResult = newResult;
- _cbSupportsSixel.CheckedState = newResult.IsSupported ? CheckState.Checked : CheckState.UnChecked;
- _pxX.Value = _sixelSupportResult.Resolution.Width;
- _pxY.Value = _sixelSupportResult.Resolution.Height;
- }
- private void SetupSixelSupported (bool isSupported)
- {
- _tabSixel.View = isSupported ? _sixelSupported : _sixelNotSupported;
- _tabView.SetNeedsDraw ();
- }
- private void BtnStartFireOnAccept (object sender, CommandEventArgs e)
- {
- if (_fire != null)
- {
- return;
- }
- if (!_sixelSupportResult.SupportsTransparency)
- {
- if (MessageBox.Query (Application.Instance,
- "Transparency Not Supported",
- "It looks like your terminal does not support transparent sixel backgrounds. Do you want to try anyway?",
- "Yes",
- "No")
- != 0)
- {
- return;
- }
- }
- _fire = new (_win.Frame.Width * _pxX.Value, _win.Frame.Height * _pxY.Value);
- _fireEncoder = new ();
- _fireEncoder.Quantizer.MaxColors = Math.Min (_fireEncoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
- _fireEncoder.Quantizer.PaletteBuildingAlgorithm = new ConstPalette (_fire.Palette);
- _fireFrameCounter = 0;
- Application.AddTimeout (TimeSpan.FromMilliseconds (30), AdvanceFireTimerCallback);
- }
- private bool AdvanceFireTimerCallback ()
- {
- _fire.AdvanceFrame ();
- _fireFrameCounter++;
- // Control frame rate by adjusting this
- // Lower number means more FPS
- if (_fireFrameCounter % 2 != 0 || _isDisposed)
- {
- return !_isDisposed;
- }
- Color [,] bmp = _fire.GetFirePixels ();
- // TODO: Static way of doing this, suboptimal
- // ConcurrentQueue doesn't support Remove, so we update the existing object
- if (_fireSixel == null)
- {
- _fireSixel = new ()
- {
- SixelData = _fireEncoder.EncodeSixel (bmp),
- ScreenPosition = new (0, 0)
- };
- Application.GetSixels ().Enqueue (_fireSixel);
- }
- else
- {
- _fireSixel.SixelData = _fireEncoder.EncodeSixel (bmp);
- _fireSixel.ScreenPosition = new (0, 0);
- }
- _win.SetNeedsDraw ();
- return !_isDisposed;
- }
- /// <inheritdoc/>
- protected override void Dispose (bool disposing)
- {
- base.Dispose (disposing);
- _imageView.Dispose ();
- _sixelNotSupported.Dispose ();
- _sixelSupported.Dispose ();
- _isDisposed = true;
- }
- private void OpenImage (object sender, CommandEventArgs e)
- {
- var ofd = new OpenDialog { Title = "Open Image", AllowsMultipleSelection = false };
- Application.Run (ofd);
- if (ofd.Path is { })
- {
- Directory.SetCurrentDirectory (Path.GetFullPath (Path.GetDirectoryName (ofd.Path)!));
- }
- if (ofd.Canceled)
- {
- ofd.Dispose ();
- return;
- }
- string path = ofd.FilePaths [0];
- ofd.Dispose ();
- if (string.IsNullOrWhiteSpace (path))
- {
- return;
- }
- if (!File.Exists (path))
- {
- return;
- }
- Image<Rgba32> img;
- try
- {
- img = Image.Load<Rgba32> (File.ReadAllBytes (path));
- }
- catch (Exception ex)
- {
- MessageBox.ErrorQuery (Application.Instance, "Could not open file", ex.Message, "Ok");
- return;
- }
- _imageView.SetImage (img);
- ApplyShowTabViewHack ();
- Application.LayoutAndDraw ();
- }
- private void ApplyShowTabViewHack ()
- {
- // TODO HACK: This hack seems to be required to make tabview actually refresh itself
- _tabView.SetNeedsDraw ();
- Tab orig = _tabView.SelectedTab;
- _tabView.SelectedTab = _tabView.Tabs.Except (new [] { orig }).ElementAt (0);
- _tabView.SelectedTab = orig;
- }
- private void BuildBasicTab (Tab tabBasic)
- {
- _imageView = new ()
- {
- Width = Dim.Fill (),
- Height = Dim.Fill (),
- CanFocus = true
- };
- tabBasic.View = _imageView;
- }
- private void BuildSixelTab ()
- {
- _sixelSupported = new ()
- {
- Width = Dim.Fill (),
- Height = Dim.Fill (),
- CanFocus = true
- };
- _sixelNotSupported = new ()
- {
- Width = Dim.Fill (),
- Height = Dim.Fill (),
- CanFocus = true
- };
- _sixelNotSupported.Add (
- new Label
- {
- Width = Dim.Fill (),
- Height = Dim.Fill (),
- TextAlignment = Alignment.Center,
- Text = "Your driver does not support Sixel image format",
- VerticalTextAlignment = Alignment.Center
- });
- _sixelView = new ()
- {
- Width = Dim.Percent (50),
- Height = Dim.Fill (),
- BorderStyle = LineStyle.Dotted
- };
- _sixelSupported.Add (_sixelView);
- var btnSixel = new Button
- {
- X = Pos.Right (_sixelView),
- Y = 0,
- Text = "Output Sixel", Width = Dim.Auto ()
- };
- btnSixel.Accepting += OutputSixelButtonClick;
- _sixelSupported.Add (btnSixel);
- var btnStartFire = new Button
- {
- X = Pos.Right (_sixelView),
- Y = Pos.Bottom (btnSixel),
- Text = "Start Fire"
- };
- btnStartFire.Accepting += BtnStartFireOnAccept;
- _sixelSupported.Add (btnStartFire);
- var lblPxX = new Label
- {
- X = Pos.Right (_sixelView),
- Y = Pos.Bottom (btnStartFire) + 1,
- Text = "Pixels per Col:"
- };
- _pxX = new ()
- {
- X = Pos.Right (lblPxX),
- Y = Pos.Bottom (btnStartFire) + 1,
- Value = _sixelSupportResult.Resolution.Width
- };
- var lblPxY = new Label
- {
- X = lblPxX.X,
- Y = Pos.Bottom (_pxX),
- Text = "Pixels per Row:"
- };
- _pxY = new ()
- {
- X = Pos.Right (lblPxY),
- Y = Pos.Bottom (_pxX),
- Value = _sixelSupportResult.Resolution.Height
- };
- var l1 = new Label
- {
- Text = "Palette Building Algorithm",
- Width = Dim.Auto (),
- X = Pos.Right (_sixelView),
- Y = Pos.Bottom (_pxY) + 1
- };
- _osPaletteBuilder = new ()
- {
- Labels =
- [
- "Popularity",
- "Median Cut"
- ],
- X = Pos.Right (_sixelView) + 2,
- Y = Pos.Bottom (l1),
- Value = 1
- };
- _popularityThreshold = new ()
- {
- X = Pos.Right (_osPaletteBuilder) + 1,
- Y = Pos.Top (_osPaletteBuilder),
- Value = 8
- };
- var lblPopThreshold = new Label
- {
- Text = "(threshold)",
- X = Pos.Right (_popularityThreshold),
- Y = Pos.Top (_popularityThreshold)
- };
- var l2 = new Label
- {
- Text = "Color Distance Algorithm",
- Width = Dim.Auto (),
- X = Pos.Right (_sixelView),
- Y = Pos.Bottom (_osPaletteBuilder) + 1
- };
- _osDistanceAlgorithm = new ()
- {
- Labels = new []
- {
- "Euclidian",
- "CIE76"
- },
- X = Pos.Right (_sixelView) + 2,
- Y = Pos.Bottom (l2)
- };
- _sixelSupported.Add (lblPxX);
- _sixelSupported.Add (_pxX);
- _sixelSupported.Add (lblPxY);
- _sixelSupported.Add (_pxY);
- _sixelSupported.Add (l1);
- _sixelSupported.Add (_osPaletteBuilder);
- _sixelSupported.Add (l2);
- _sixelSupported.Add (_osDistanceAlgorithm);
- _sixelSupported.Add (_popularityThreshold);
- _sixelSupported.Add (lblPopThreshold);
- _sixelView.DrawingContent += SixelViewOnDrawingContent;
- }
- private IPaletteBuilder GetPaletteBuilder ()
- {
- switch (_osPaletteBuilder.Value)
- {
- case 0: return new PopularityPaletteWithThreshold (GetDistanceAlgorithm (), _popularityThreshold.Value);
- case 1: return new MedianCutPaletteBuilder (GetDistanceAlgorithm ());
- default: throw new ArgumentOutOfRangeException ();
- }
- }
- private IColorDistance GetDistanceAlgorithm ()
- {
- switch (_osDistanceAlgorithm.Value)
- {
- case 0: return new EuclideanColorDistance ();
- case 1: return new CIE76ColorDistance ();
- default: throw new ArgumentOutOfRangeException ();
- }
- }
- private void OutputSixelButtonClick (object sender, CommandEventArgs e)
- {
- if (_imageView.FullResImage == null)
- {
- MessageBox.Query (Application.Instance, "No Image Loaded", "You must first open an image. Use the 'Open Image' button above.", "Ok");
- return;
- }
- _screenLocationForSixel = _sixelView.FrameToScreen ().Location;
- _encodedSixelData = GenerateSixelData (
- _imageView.FullResImage,
- _sixelView.Frame.Size,
- _pxX.Value,
- _pxY.Value);
- if (_sixelImage == null)
- {
- _sixelImage = new ()
- {
- SixelData = _encodedSixelData,
- ScreenPosition = _screenLocationForSixel
- };
- Application.GetSixels ().Enqueue (_sixelImage);
- }
- else
- {
- _sixelImage.ScreenPosition = _screenLocationForSixel;
- _sixelImage.SixelData = _encodedSixelData;
- }
- _sixelView.SetNeedsDraw ();
- }
- private void SixelViewOnDrawingContent (object sender, DrawEventArgs e)
- {
- if (!string.IsNullOrWhiteSpace (_encodedSixelData))
- {
- // Does not work (see https://github.com/gui-cs/Terminal.Gui/issues/3763)
- // Application.Driver?.Move (_screenLocationForSixel.X, _screenLocationForSixel.Y);
- // Application.Driver?.AddStr (_encodedSixelData);
- // Works in DotNetDriver but results in screen flicker when moving mouse but vanish instantly
- // Console.SetCursorPosition (_screenLocationForSixel.X, _screenLocationForSixel.Y);
- // Console.Write (_encodedSixelData);
- }
- }
- public string GenerateSixelData (
- Image<Rgba32> fullResImage,
- Size maxSize,
- int pixelsPerCellX,
- int pixelsPerCellY
- )
- {
- var encoder = new SixelEncoder ();
- encoder.Quantizer.MaxColors = Math.Min (encoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
- encoder.Quantizer.PaletteBuildingAlgorithm = GetPaletteBuilder ();
- encoder.Quantizer.DistanceAlgorithm = GetDistanceAlgorithm ();
- // Calculate the target size in pixels based on console units
- int targetWidthInPixels = maxSize.Width * pixelsPerCellX;
- int targetHeightInPixels = maxSize.Height * pixelsPerCellY;
- // Get the original image dimensions
- int originalWidth = fullResImage.Width;
- int originalHeight = fullResImage.Height;
- // Use the helper function to get the resized dimensions while maintaining the aspect ratio
- Size newSize = CalculateAspectRatioFit (originalWidth, originalHeight, targetWidthInPixels, targetHeightInPixels);
- // Resize the image to match the console size
- Image<Rgba32> resizedImage = fullResImage.Clone (x => x.Resize (newSize.Width, newSize.Height));
- string encoded = encoder.EncodeSixel (ConvertToColorArray (resizedImage));
- var pv = new PaletteView (encoder.Quantizer.Palette.ToList ());
- var dlg = new Dialog
- {
- Title = "Palette (Esc to close)",
- Width = Dim.Fill (2),
- Height = Dim.Fill (1)
- };
- var btn = new Button
- {
- Text = "Ok"
- };
- btn.Accepting += (s, e) => Application.RequestStop ();
- dlg.Add (pv);
- dlg.AddButton (btn);
- Application.Run (dlg);
- dlg.Dispose ();
- return encoded;
- }
- private Size CalculateAspectRatioFit (int originalWidth, int originalHeight, int targetWidth, int targetHeight)
- {
- // Calculate the scaling factor for width and height
- double widthScale = (double)targetWidth / originalWidth;
- double heightScale = (double)targetHeight / originalHeight;
- // Use the smaller scaling factor to maintain the aspect ratio
- double scale = Math.Min (widthScale, heightScale);
- // Calculate the new width and height while keeping the aspect ratio
- var newWidth = (int)(originalWidth * scale);
- var newHeight = (int)(originalHeight * scale);
- // Return the new size as a Size object
- return new (newWidth, newHeight);
- }
- public static Color [,] ConvertToColorArray (Image<Rgba32> image)
- {
- int width = image.Width;
- int height = image.Height;
- Color [,] colors = new Color [width, height];
- // Loop through each pixel and convert Rgba32 to Terminal.Gui color
- for (var x = 0; x < width; x++)
- {
- for (var y = 0; y < height; y++)
- {
- Rgba32 pixel = image [x, y];
- colors [x, y] = new (pixel.R, pixel.G, pixel.B); // Convert Rgba32 to Terminal.Gui color
- }
- }
- return colors;
- }
- private class ImageView : View
- {
- private readonly ConcurrentDictionary<Rgba32, Attribute> _cache = new ();
- public Image<Rgba32> FullResImage;
- private Image<Rgba32> _matchSize;
- protected override bool OnDrawingContent (DrawContext context)
- {
- if (FullResImage == null)
- {
- return true;
- }
- // if we have not got a cached resized image of this size
- if (_matchSize == null || Viewport.Width != _matchSize.Width || Viewport.Height != _matchSize.Height)
- {
- // generate one
- _matchSize = FullResImage.Clone (x => x.Resize (Viewport.Width, Viewport.Height));
- }
- for (var y = 0; y < Viewport.Height; y++)
- {
- for (var x = 0; x < Viewport.Width; x++)
- {
- Rgba32 rgb = _matchSize [x, y];
- Attribute attr = _cache.GetOrAdd (
- rgb,
- rgb => new (
- new Color (),
- new Color (rgb.R, rgb.G, rgb.B)
- )
- );
- SetAttribute (attr);
- AddRune (x, y, (Rune)' ');
- }
- }
- return true;
- }
- internal void SetImage (Image<Rgba32> image)
- {
- FullResImage = image;
- SetNeedsDraw ();
- }
- }
- public class PaletteView : View
- {
- private readonly List<Color> _palette;
- public PaletteView (List<Color> palette)
- {
- _palette = palette ?? new List<Color> ();
- Width = Dim.Fill ();
- Height = Dim.Fill ();
- }
- // Automatically calculates rows and columns based on the available bounds
- private (int columns, int rows) CalculateGridSize (Rectangle bounds)
- {
- // Characters are twice as wide as they are tall, so use 2:1 width-to-height ratio
- int availableWidth = Viewport.Width / 2; // Each color block is 2 character wide
- int availableHeight = Viewport.Height;
- int numColors = _palette.Count;
- // Calculate the number of columns and rows we can fit within the bounds
- int columns = Math.Min (availableWidth, numColors);
- int rows = (numColors + columns - 1) / columns; // Ceiling division for rows
- // Ensure we do not exceed the available height
- if (rows > availableHeight)
- {
- rows = availableHeight;
- columns = (numColors + rows - 1) / rows; // Recalculate columns if needed
- }
- return (columns, rows);
- }
- protected override bool OnDrawingContent (DrawContext context)
- {
- if (_palette == null || _palette.Count == 0)
- {
- return false;
- }
- // Calculate the grid size based on the bounds
- (int columns, int rows) = CalculateGridSize (Viewport);
- // Draw the colors in the palette
- for (var i = 0; i < _palette.Count && i < columns * rows; i++)
- {
- int row = i / columns;
- int col = i % columns;
- // Calculate position in the grid
- int x = col * 2; // Each color block takes up 2 horizontal spaces
- int y = row;
- // Set the color attribute for the block
- SetAttribute (new (_palette [i], _palette [i]));
- // Draw the block (2 characters wide per block)
- for (var dx = 0; dx < 2; dx++) // Fill the width of the block
- {
- AddRune (x + dx, y, (Rune)' ');
- }
- }
- return true;
- }
- }
- }
- internal class ConstPalette : IPaletteBuilder
- {
- private readonly List<Color> _palette;
- public ConstPalette (Color [] palette) { _palette = palette.ToList (); }
- /// <inheritdoc/>
- public List<Color> BuildPalette (List<Color> colors, int maxColors) { return _palette; }
- }
- public abstract class LabColorDistance : IColorDistance
- {
- // Reference white point for D65 illuminant (can be moved to constants)
- private const double RefX = 95.047;
- private const double RefY = 100.000;
- private const double RefZ = 108.883;
- // Conversion from RGB to Lab
- protected LabColor RgbToLab (Color c)
- {
- XYZ xyz = ColorConverter.RgbToXyz (new (c.R, c.G, c.B));
- // Normalize XYZ values by reference white point
- double x = xyz.X / RefX;
- double y = xyz.Y / RefY;
- double z = xyz.Z / RefZ;
- // Apply the nonlinear transformation for Lab
- x = x > 0.008856 ? Math.Pow (x, 1.0 / 3.0) : 7.787 * x + 16.0 / 116.0;
- y = y > 0.008856 ? Math.Pow (y, 1.0 / 3.0) : 7.787 * y + 16.0 / 116.0;
- z = z > 0.008856 ? Math.Pow (z, 1.0 / 3.0) : 7.787 * z + 16.0 / 116.0;
- // Calculate Lab values
- double l = 116.0 * y - 16.0;
- double a = 500.0 * (x - y);
- double b = 200.0 * (y - z);
- return new (l, a, b);
- }
- // LabColor class encapsulating L, A, and B values
- protected class LabColor
- {
- public double L { get; }
- public double A { get; }
- public double B { get; }
- public LabColor (double l, double a, double b)
- {
- L = l;
- A = a;
- B = b;
- }
- }
- /// <inheritdoc/>
- public abstract double CalculateDistance (Color c1, Color c2);
- }
- /// <summary>
- /// This is the simplest method to measure color difference in the CIE Lab color space. The Euclidean distance in Lab
- /// space is more aligned with human perception than RGB space, as Lab attempts to model how humans perceive color
- /// differences.
- /// </summary>
- public class CIE76ColorDistance : LabColorDistance
- {
- public override double CalculateDistance (Color c1, Color c2)
- {
- LabColor lab1 = RgbToLab (c1);
- LabColor lab2 = RgbToLab (c2);
- // Euclidean distance in Lab color space
- return Math.Sqrt (Math.Pow (lab1.L - lab2.L, 2) + Math.Pow (lab1.A - lab2.A, 2) + Math.Pow (lab1.B - lab2.B, 2));
- }
- }
- public class MedianCutPaletteBuilder : IPaletteBuilder
- {
- private readonly IColorDistance _colorDistance;
- public MedianCutPaletteBuilder (IColorDistance colorDistance) { _colorDistance = colorDistance; }
- public List<Color> BuildPalette (List<Color> colors, int maxColors)
- {
- if (colors == null || colors.Count == 0 || maxColors <= 0)
- {
- return new ();
- }
- return MedianCut (colors, maxColors);
- }
- private List<Color> MedianCut (List<Color> colors, int maxColors)
- {
- List<List<Color>> cubes = new () { colors };
- // Recursively split color regions
- while (cubes.Count < maxColors)
- {
- var added = false;
- cubes.Sort ((a, b) => Volume (a).CompareTo (Volume (b)));
- List<Color> largestCube = cubes.Last ();
- cubes.RemoveAt (cubes.Count - 1);
- // Check if the largest cube contains only one unique color
- if (IsSingleColorCube (largestCube))
- {
- // Add back and stop splitting this cube
- cubes.Add (largestCube);
- break;
- }
- (List<Color> cube1, List<Color> cube2) = SplitCube (largestCube);
- if (cube1.Any ())
- {
- cubes.Add (cube1);
- added = true;
- }
- if (cube2.Any ())
- {
- cubes.Add (cube2);
- added = true;
- }
- // Break the loop if no new cubes were added
- if (!added)
- {
- break;
- }
- }
- // Calculate average color for each cube
- return cubes.Select (AverageColor).Distinct ().ToList ();
- }
- // Checks if all colors in the cube are the same
- private bool IsSingleColorCube (List<Color> cube)
- {
- Color firstColor = cube.First ();
- return cube.All (c => c.R == firstColor.R && c.G == firstColor.G && c.B == firstColor.B);
- }
- // Splits the cube based on the largest color component range
- private (List<Color>, List<Color>) SplitCube (List<Color> cube)
- {
- (int component, int range) = FindLargestRange (cube);
- // Sort by the largest color range component (either R, G, or B)
- cube.Sort (
- (c1, c2) => component switch
- {
- 0 => c1.R.CompareTo (c2.R),
- 1 => c1.G.CompareTo (c2.G),
- 2 => c1.B.CompareTo (c2.B),
- _ => 0
- });
- int medianIndex = cube.Count / 2;
- List<Color> cube1 = cube.Take (medianIndex).ToList ();
- List<Color> cube2 = cube.Skip (medianIndex).ToList ();
- return (cube1, cube2);
- }
- private (int, int) FindLargestRange (List<Color> cube)
- {
- byte minR = cube.Min (c => c.R);
- byte maxR = cube.Max (c => c.R);
- byte minG = cube.Min (c => c.G);
- byte maxG = cube.Max (c => c.G);
- byte minB = cube.Min (c => c.B);
- byte maxB = cube.Max (c => c.B);
- int rangeR = maxR - minR;
- int rangeG = maxG - minG;
- int rangeB = maxB - minB;
- if (rangeR >= rangeG && rangeR >= rangeB)
- {
- return (0, rangeR);
- }
- if (rangeG >= rangeR && rangeG >= rangeB)
- {
- return (1, rangeG);
- }
- return (2, rangeB);
- }
- private Color AverageColor (List<Color> cube)
- {
- var avgR = (byte)cube.Average (c => c.R);
- var avgG = (byte)cube.Average (c => c.G);
- var avgB = (byte)cube.Average (c => c.B);
- return new (avgR, avgG, avgB);
- }
- private int Volume (List<Color> cube)
- {
- if (cube == null || cube.Count == 0)
- {
- // Return a volume of 0 if the cube is empty or null
- return 0;
- }
- byte minR = cube.Min (c => c.R);
- byte maxR = cube.Max (c => c.R);
- byte minG = cube.Min (c => c.G);
- byte maxG = cube.Max (c => c.G);
- byte minB = cube.Min (c => c.B);
- byte maxB = cube.Max (c => c.B);
- return (maxR - minR) * (maxG - minG) * (maxB - minB);
- }
- }
- public class DoomFire
- {
- private readonly int _width;
- private readonly int _height;
- private readonly Color [,] _firePixels;
- private static Color [] _palette;
- public Color [] Palette => _palette;
- private readonly Random _random = new ();
- public DoomFire (int width, int height)
- {
- _width = width;
- _height = height;
- _firePixels = new Color [width, height];
- InitializePalette ();
- InitializeFire ();
- }
- private void InitializePalette ()
- {
- // Initialize a basic fire palette. You can modify these colors as needed.
- _palette = new Color [37]; // Using 37 colors as per the original Doom fire palette scale.
- // First color is transparent black
- _palette [0] = new (0, 0, 0, 0); // Transparent black (ARGB)
- // The rest of the palette is fire colors
- for (var i = 1; i < 37; i++)
- {
- var r = (byte)Math.Min (255, i * 7);
- var g = (byte)Math.Min (255, i * 5);
- var b = (byte)Math.Min (255, i * 2);
- _palette [i] = new (r, g, b); // Full opacity
- }
- }
- public void InitializeFire ()
- {
- // Set the bottom row to full intensity (simulate the base of the fire).
- for (var x = 0; x < _width; x++)
- {
- _firePixels [x, _height - 1] = _palette [36]; // Max intensity fire.
- }
- // Set the rest of the pixels to black (transparent).
- for (var y = 0; y < _height - 1; y++)
- {
- for (var x = 0; x < _width; x++)
- {
- _firePixels [x, y] = _palette [0]; // Transparent black
- }
- }
- }
- public void AdvanceFrame ()
- {
- // Process every pixel except the bottom row
- for (var x = 0; x < _width; x++)
- {
- for (var y = 1; y < _height; y++) // Skip the last row (which is always max intensity)
- {
- int srcX = x;
- int srcY = y;
- int dstY = y - 1;
- // Spread fire upwards with randomness
- int decay = _random.Next (0, 2);
- int dstX = srcX + _random.Next (-1, 2);
- if (dstX < 0 || dstX >= _width) // Prevent out of bounds
- {
- dstX = srcX;
- }
- // Get the fire color from below and reduce its intensity
- Color srcColor = _firePixels [srcX, srcY];
- int intensity = Array.IndexOf (_palette, srcColor) - decay;
- if (intensity < 0)
- {
- intensity = 0;
- }
- _firePixels [dstX, dstY] = _palette [intensity];
- }
- }
- }
- public Color [,] GetFirePixels () { return _firePixels; }
- }
|