diff --git a/Terminal.Gui/Drawing/SixelEncoder.cs b/Terminal.Gui/Drawing/SixelEncoder.cs index 5929752df..77f623d27 100644 --- a/Terminal.Gui/Drawing/SixelEncoder.cs +++ b/Terminal.Gui/Drawing/SixelEncoder.cs @@ -113,13 +113,24 @@ public class SixelEncoder for (int x = 0; x < width; ++x) { Array.Clear (code, 0, usedColorIdx.Count); + bool anyNonTransparentPixel = false; // Track if any non-transparent pixels are found in this column // Process each row in the 6-pixel high band for (int row = 0; row < bandHeight; ++row) { var color = pixels [x, startY + row]; + int colorIndex = Quantizer.GetNearestColor (color); + if (color.A == 0) // Skip fully transparent pixels + { + continue; + } + else + { + anyNonTransparentPixel = true; + } + if (slots [colorIndex] == -1) { targets.Add (new List ()); @@ -135,6 +146,8 @@ public class SixelEncoder code [slots [colorIndex]] |= (byte)(1 << row); // Accumulate SIXEL data } + // TODO: Handle fully empty rows better + // Handle transitions between columns for (int j = 0; j < usedColorIdx.Count; ++j) { diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs index 0c79df0c4..817e4cfa5 100644 --- a/UICatalog/Scenarios/Images.cs +++ b/UICatalog/Scenarios/Images.cs @@ -22,11 +22,12 @@ public class Images : Scenario private ImageView _imageView; private Point _screenLocationForSixel; private string _encodedSixelData; + private Window _win; public override void Main () { Application.Init (); - var win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; + _win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false; @@ -41,7 +42,7 @@ public class Images : Scenario }; var lblDriverName = new Label { X = 0, Y = 0, Text = $"Driver is {Application.Driver?.GetType ().Name}" }; - win.Add (lblDriverName); + _win.Add (lblDriverName); var cbSupportsTrueColor = new CheckBox { @@ -51,7 +52,7 @@ public class Images : Scenario CanFocus = false, Text = "supports true color " }; - win.Add (cbSupportsTrueColor); + _win.Add (cbSupportsTrueColor); var cbSupportsSixel = new CheckBox { @@ -63,7 +64,7 @@ public class Images : Scenario Enabled = false, Text = "Supports Sixel" }; - win.Add (cbSupportsSixel); + _win.Add (cbSupportsSixel); var cbUseTrueColor = new CheckBox { @@ -74,10 +75,15 @@ public class Images : Scenario Text = "Use true color" }; cbUseTrueColor.CheckedStateChanging += (_, evt) => Application.Force16Colors = evt.NewValue == CheckState.UnChecked; - win.Add (cbUseTrueColor); + _win.Add (cbUseTrueColor); var btnOpenImage = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 0, Text = "Open Image" }; - win.Add (btnOpenImage); + _win.Add (btnOpenImage); + + var btnStartFire = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 1, Text = "Start Fire" }; + _win.Add (btnStartFire); + + btnStartFire.Accept += BtnStartFireOnAccept; var tv = new TabView { @@ -92,11 +98,38 @@ public class Images : Scenario btnOpenImage.Accept += OpenImage; - win.Add (tv); - Application.Run (win); - win.Dispose (); + _win.Add (tv); + Application.Run (_win); + _win.Dispose (); Application.Shutdown (); + } + private void BtnStartFireOnAccept (object sender, HandledEventArgs e) + { + var fire = new DoomFire (_win.Frame.Width, _win.Frame.Height); + var encoder = new SixelEncoder (); + encoder.Quantizer.PaletteBuildingAlgorithm = new ConstPalette (fire.Palette); + + Application.AddTimeout ( + TimeSpan.FromMilliseconds (500), + () => + { + fire.AdvanceFrame (); + + var bmp = fire.GetFirePixels (); + + // TODO: Static way of doing this, suboptimal + Application.Sixel.Clear (); + Application.Sixel.Add (new SixelToRender + { + SixelData = encoder.EncodeSixel (bmp), + ScreenPosition = new Point (0,0) + }); + + _win.SetNeedsDisplay(); + + return true; + }); } /// @@ -150,6 +183,7 @@ public class Images : Scenario return; } + _imageView.SetImage (img); Application.Refresh (); } @@ -465,6 +499,19 @@ public class Images : Scenario } } +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) @@ -677,3 +724,93 @@ public class MedianCutPaletteBuilder : IPaletteBuilder return (maxR - minR) * (maxG - minG) * (maxB - minB); } } + + +public class DoomFire +{ + private int _width; + private int _height; + private Color [,] _firePixels; + private static Color [] _palette; + public Color [] Palette => _palette; + + 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 Color (0, 0, 0, 0); // Transparent black (ARGB) + + // The rest of the palette is fire colors + for (int i = 1; i < 37; i++) + { + byte r = (byte)Math.Min (255, i * 7); + byte g = (byte)Math.Min (255, i * 5); + byte b = (byte)Math.Min (255, i * 2); + _palette [i] = new Color (r, g, b); // Full opacity + } + } + + public void InitializeFire () + { + // Set the bottom row to full intensity (simulate the base of the fire). + for (int x = 0; x < _width; x++) + { + _firePixels [x, _height - 1] = _palette [36]; // Max intensity fire. + } + + // Set the rest of the pixels to black (transparent). + for (int y = 0; y < _height - 1; y++) + { + for (int x = 0; x < _width; x++) + { + _firePixels [x, y] = _palette [0]; // Transparent black + } + } + } + + public void AdvanceFrame () + { + // Process every pixel except the bottom row + for (int x = 0; x < _width; x++) + { + for (int 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 = new Random ().Next (0, 3); + int dstX = Math.Max (0, srcX - decay); + + // 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; + } +} + diff --git a/UnitTests/Drawing/SixelEncoderTests.cs b/UnitTests/Drawing/SixelEncoderTests.cs index 3e84f5b8c..100eed03d 100644 --- a/UnitTests/Drawing/SixelEncoderTests.cs +++ b/UnitTests/Drawing/SixelEncoderTests.cs @@ -149,4 +149,86 @@ public class SixelEncoderTests // Compare the generated SIXEL string with the expected one Assert.Equal (expected, result); } + + [Fact] + public void EncodeSixel_Transparent12x12_ReturnsExpectedSixel () + { + string expected = "\u001bP" // Start sixel sequence + + "0;0;0" // Defaults for aspect ratio and grid size + + "q" // Signals beginning of sixel image data + + "\"1;1;12;12" // no scaling factors (1x1) and filling 12x12 pixel area + + "#0;2;0;0;0" // Black transparent (TODO: Shouldn't really be output this if it is transparent) + // Since all pixels are transparent, the data should just be filled with '?' + + "#0!12?$-" // Fills the transparent line with byte 0 which maps to '?' + + "#0!12?$" // Second band, same fully transparent pixels + + "\u001b\\"; // End sixel sequence + + // Arrange: Create a 12x12 bitmap filled with fully transparent pixels + Color [,] pixels = new Color [12, 12]; + + for (var x = 0; x < 12; x++) + { + for (var y = 0; y < 12; y++) + { + pixels [x, y] = new (0, 0, 0, 0); // Fully transparent + } + } + + // Act: Encode the image + var encoder = new SixelEncoder (); + string result = encoder.EncodeSixel (pixels); + + // Assert: Expect the result to be fully transparent encoded output + Assert.Equal (expected, result); + } + [Fact] + public void EncodeSixel_VerticalMix_TransparentAndColor_ReturnsExpectedSixel () + { + string expected = "\u001bP" // Start sixel sequence + + "0;0;0" // Defaults for aspect ratio and grid size + + "q" // Signals beginning of sixel image data + + "\"1;1;12;12" // No scaling factors (1x1) and filling 12x12 pixel area + /* + * Define the color palette: + * We'll use one color (Red) for the colored pixels. + */ + + "#0;2;100;0;0" // Red color definition (index 0: RGB 100,0,0) + + "#1;2;0;0;0" // Black transparent (TODO: Shouldn't really be output this if it is transparent) + /* + * Start of the Pixel data + * We have alternating transparent (0) and colored (red) pixels in a vertical band. + * The pattern for each sixel byte is 101010, which in binary (+63) converts to ASCII character 'T'. + * Since we have 12 pixels horizontally, we'll see this pattern repeat across the row so we see + * the 'sequence repeat' 12 times i.e. !12 (do the next letter 'T' 12 times). + */ + + "#0!12T$-" // First band of alternating red and transparent pixels + + "#0!12T$" // Second band, same alternating red and transparent pixels + + "\u001b\\"; // End sixel sequence + + // Arrange: Create a 12x12 bitmap with alternating transparent and red pixels in a vertical band + Color [,] pixels = new Color [12, 12]; + + for (var x = 0; x < 12; x++) + { + for (var y = 0; y < 12; y++) + { + // For simplicity, we'll make every other row transparent + if (y % 2 == 0) + { + pixels [x, y] = new (255, 0, 0); // Red pixel + } + else + { + pixels [x, y] = new (0, 0, 0, 0); // Transparent pixel + } + } + } + + // Act: Encode the image + var encoder = new SixelEncoder (); + string result = encoder.EncodeSixel (pixels); + + // Assert: Expect the result to match the expected sixel output + Assert.Equal (expected, result); + } }