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;
///
/// Number of sixel pixels per row of characters in the console.
///
private NumericUpDown _pxY;
///
/// Number of sixel pixels per column of characters in the console
///
private NumericUpDown _pxX;
///
/// View shown in sixel tab if sixel is supported
///
private View _sixelSupported;
///
/// View shown in sixel tab if sixel is not supported
///
private View _sixelNotSupported;
private Tab _tabSixel;
private TabView _tabView;
///
/// The view into which the currently opened sixel image is bounded
///
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;
}
///
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 img;
try
{
img = Image.Load (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 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 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 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 _cache = new ();
public Image FullResImage;
private Image _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 image)
{
FullResImage = image;
SetNeedsDraw ();
}
}
public class PaletteView : View
{
private readonly List _palette;
public PaletteView (List palette)
{
_palette = palette ?? new List ();
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 _palette;
public ConstPalette (Color [] palette) { _palette = palette.ToList (); }
///
public List BuildPalette (List 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;
}
}
///
public abstract double CalculateDistance (Color c1, Color c2);
}
///
/// 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.
///
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 BuildPalette (List colors, int maxColors)
{
if (colors == null || colors.Count == 0 || maxColors <= 0)
{
return new ();
}
return MedianCut (colors, maxColors);
}
private List MedianCut (List colors, int maxColors)
{
List> cubes = new () { colors };
// Recursively split color regions
while (cubes.Count < maxColors)
{
var added = false;
cubes.Sort ((a, b) => Volume (a).CompareTo (Volume (b)));
List 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 cube1, List 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 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, List) SplitCube (List 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 cube1 = cube.Take (medianIndex).ToList ();
List cube2 = cube.Skip (medianIndex).ToList ();
return (cube1, cube2);
}
private (int, int) FindLargestRange (List 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 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 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; }
}