diff --git a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs
index b6b1d89..1f51862 100644
--- a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs
+++ b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@@ -59,22 +60,37 @@ public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect
var targetRect = new Rect(Transform.Transform(new Point(destRect.TopLeft.X, destRect.TopLeft.Y)),
Transform.Transform(new Point(destRect.BottomRight.X, destRect.BottomRight.Y)));
var bmp = (BitmapImpl)source;
- using var bitmap = new SKBitmap((int)targetRect.Width, (int)targetRect.Height);
+
+ // resize source to be target rect * 2 so we can map to quad pixels
+ using var bitmap = new SKBitmap((int)targetRect.Width * 2, (int)targetRect.Height * 2);
using var canvas = new SKCanvas(bitmap);
- canvas.DrawBitmap(bmp.Bitmap, new SKRect(0, 0, (float)targetRect.Width, (float)targetRect.Height),
+ canvas.DrawBitmap(bmp.Bitmap, new SKRect(0, 0, bitmap.Width, bitmap.Height),
new SKPaint { FilterQuality = SKFilterQuality.Medium });
- // Rect clip = CurrentClip.Intersect(destRect);
- int width = bitmap.Info.Width;
- int height = bitmap.Info.Height;
- for (int x = 0; x < width; x++)
- for (int y = 0; y < height; y++)
+ for (int y = 0; y < bitmap.Info.Height; y += 2)
+ for (int x = 0; x < bitmap.Info.Width; x += 2)
{
- int px = (int)targetRect.TopLeft.X + x;
- int py = (int)targetRect.TopLeft.Y + y;
- SKColor skColor = bitmap.GetPixel(x, y);
- Color color = Color.FromRgb(skColor.Red, skColor.Green, skColor.Blue);
- var imagePixel = new Pixel('█', color);
+ // NOTE: we divide by 2 because we are working with quad pixels,
+ // // the bitmap has twice the horizontal and twice the vertical of the target rect.
+ int px = (int)targetRect.TopLeft.X + x / 2;
+ int py = (int)targetRect.TopLeft.Y + y / 2;
+
+ // get the quad pixel the bitmap
+ var quadColors = new[]
+ {
+ bitmap.GetPixel(x, y), bitmap.GetPixel(x + 1, y),
+ bitmap.GetPixel(x, y + 1), bitmap.GetPixel(x + 1, y + 1)
+ };
+
+ // map it to a single char to represet the 4 pixels
+ char quadPixel = GetQuadPixelCharacter(quadColors);
+
+ // get the combined colors for the quad pixel
+ Color foreground = GetForegroundColorForQuadPixel(quadColors, quadPixel);
+ Color background = GetBackgroundColorForQuadPixel(quadColors, quadPixel);
+
+ var imagePixel = new Pixel(new PixelForeground(new SimpleSymbol(quadPixel), color: foreground),
+ new PixelBackground(background));
CurrentClip.ExecuteWithClipping(new Point(px, py),
() =>
{
@@ -495,5 +511,231 @@ private void DrawPixelAndMoveHead(ref Point head, Line line, LineStyle? lineStyl
}
}
}
+
+ ///
+ /// given 4 colors return quadPixel character which is suitable to represent the colors
+ ///
+ ///
+ ///
+ ///
+ private static char GetQuadPixelCharacter(params SKColor[] colors)
+ {
+ char character = GetColorsPattern(colors) switch
+ {
+ "FFFF" => ' ',
+ "TFFF" => '▘',
+ "FTFF" => '▝',
+ "FFTF" => '▖',
+ "FFFT" => '▗',
+ "TFFT" => '▚',
+ "FTTF" => '▞',
+ "TFTF" => '▌',
+ "FTFT" => '▐',
+ "FFTT" => '▄',
+ "TTFF" => '▀',
+ "TTTF" => '▛',
+ "TTFT" => '▜',
+ "TFTT" => '▙',
+ "FTTT" => '▟',
+ "TTTT" => '█',
+ _ => throw new NotImplementedException()
+ };
+ return character;
+ }
+
+
+ ///
+ /// Combine the colors for the white part of the quad pixel character.
+ ///
+ /// 4 colors
+ ///
+ /// foreground color
+ ///
+ private static Color GetForegroundColorForQuadPixel(SKColor[] pixelColors, char quadPixel)
+ {
+ if (pixelColors.Length != 4)
+ throw new ArgumentException($"{nameof(pixelColors)} must have 4 elements.");
+
+ SKColor skColor = quadPixel switch
+ {
+ ' ' => SKColors.Transparent,
+ '▘' => pixelColors[0],
+ '▝' => pixelColors[1],
+ '▖' => pixelColors[2],
+ '▗' => pixelColors[3],
+ '▚' => CombineColors(pixelColors[0], pixelColors[2]),
+ '▞' => CombineColors(pixelColors[1], pixelColors[3]),
+ '▌' => CombineColors(pixelColors[0], pixelColors[2]),
+ '▐' => CombineColors(pixelColors[1], pixelColors[3]),
+ '▄' => CombineColors(pixelColors[2], pixelColors[3]),
+ '▀' => CombineColors(pixelColors[0], pixelColors[1]),
+ '▛' => CombineColors(pixelColors[0], pixelColors[1], pixelColors[2]),
+ '▜' => CombineColors(pixelColors[0], pixelColors[1], pixelColors[3]),
+ '▙' => CombineColors(pixelColors[0], pixelColors[2], pixelColors[3]),
+ '▟' => CombineColors(pixelColors[1], pixelColors[2], pixelColors[3]),
+ '█' => CombineColors(pixelColors.ToArray()),
+ _ => throw new NotImplementedException()
+ };
+
+ return Color.FromRgb(skColor.Red, skColor.Green, skColor.Blue);
+ }
+
+
+ ///
+ /// Combine the colors for the black part of the quad pixel character.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static Color GetBackgroundColorForQuadPixel(SKColor[] pixelColors, char quadPixel)
+ {
+ SKColor skColor = quadPixel switch
+ {
+ ' ' => CombineColors(pixelColors.ToArray()),
+ '▘' => CombineColors(pixelColors[1], pixelColors[2], pixelColors[3]),
+ '▝' => CombineColors(pixelColors[0], pixelColors[2], pixelColors[3]),
+ '▖' => CombineColors(pixelColors[0], pixelColors[1], pixelColors[3]),
+ '▗' => CombineColors(pixelColors[0], pixelColors[1], pixelColors[2]),
+ '▚' => CombineColors(pixelColors[1], pixelColors[2]),
+ '▞' => CombineColors(pixelColors[0], pixelColors[3]),
+ '▌' => CombineColors(pixelColors[1], pixelColors[3]),
+ '▐' => CombineColors(pixelColors[0], pixelColors[2]),
+ '▄' => CombineColors(pixelColors[0], pixelColors[1]),
+ '▀' => CombineColors(pixelColors[2], pixelColors[3]),
+ '▛' => pixelColors[3],
+ '▜' => pixelColors[2],
+ '▙' => pixelColors[1],
+ '▟' => pixelColors[0],
+ '█' => SKColors.Transparent,
+ _ => throw new NotImplementedException()
+ };
+ return Color.FromArgb(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
+ }
+
+ //private static SKColor CombineColors(params SKColor[] colors)
+ //{
+ // return new SKColor((byte)colors.Average(c => c.Red),
+ // (byte)colors.Average(c => c.Green),
+ // (byte)colors.Average(c => c.Blue),
+ // (byte)colors.Average(c => c.Alpha));
+ //}
+
+ private static SKColor CombineColors(params SKColor[] colors)
+ {
+ float finalRed = 0;
+ float finalGreen = 0;
+ float finalBlue = 0;
+ float finalAlpha = 0;
+
+ foreach (SKColor color in colors)
+ {
+ float alphaRatio = color.Alpha / 255.0f;
+ finalRed = (finalRed * finalAlpha + color.Red * alphaRatio) / (finalAlpha + alphaRatio);
+ finalGreen = (finalGreen * finalAlpha + color.Green * alphaRatio) / (finalAlpha + alphaRatio);
+ finalBlue = (finalBlue * finalAlpha + color.Blue * alphaRatio) / (finalAlpha + alphaRatio);
+ finalAlpha += alphaRatio * (1 - finalAlpha);
+ }
+
+ byte red = (byte)Math.Clamp(finalRed, 0, 255);
+ byte green = (byte)Math.Clamp(finalGreen, 0, 255);
+ byte blue = (byte)Math.Clamp(finalBlue, 0, 255);
+ byte alpha = (byte)Math.Clamp(finalAlpha * 255, 0, 255);
+
+ return new SKColor(red, green, blue, alpha);
+ }
+
+ ///
+ /// Cluster quad colors into a pattern (like: TTFF) based on relative closeness
+ ///
+ ///
+ /// T or F for each color as a string
+ ///
+ private static string GetColorsPattern(SKColor[] colors)
+ {
+ if (colors.Length != 4) throw new ArgumentException("Array must contain exactly 4 colors.");
+
+ // Initial guess: two clusters with the first two colors as centers
+ SKColor[] clusterCenters = { colors[0], colors[1] };
+ int[] clusters = new int[colors.Length];
+
+ for (int iteration = 0; iteration < 10; iteration++) // limit iterations to avoid infinite loop
+ {
+ // Assign colors to the closest cluster center
+ for (int i = 0; i < colors.Length; i++) clusters[i] = GetColorCluster(colors[i], clusterCenters);
+
+ // Recalculate cluster centers
+ var newClusterCenters = new SKColor[2];
+ for (int cluster = 0; cluster < 2; cluster++)
+ {
+ var clusteredColors = colors.Where((_, i) => clusters[i] == cluster).ToList();
+ if (clusteredColors.Any())
+ newClusterCenters[cluster] = GetAverageColor(clusteredColors);
+ if (clusteredColors.Count == 4)
+ if (clusteredColors.All(c => c.Alpha == 0))
+ return "FFFF";
+ // return "TTTT";
+ }
+
+ // Check for convergence
+ if (newClusterCenters.SequenceEqual(clusterCenters))
+ break;
+
+ clusterCenters = newClusterCenters;
+ }
+
+ // Determine which cluster is lower and which is higher
+ int lowerCluster = GetColorBrightness(clusterCenters[0]) < GetColorBrightness(clusterCenters[1]) ? 0 : 1;
+ int higherCluster = 1 - lowerCluster;
+
+ // Replace colors with 0 for lower cluster and 1 for higher cluster
+ var sb = new StringBuilder();
+ for (int i = 0; i < colors.Length; i++) sb.Append(clusters[i] == higherCluster ? 'T' : 'F');
+
+ return sb.ToString();
+ }
+
+ private static int GetColorCluster(SKColor color, SKColor[] clusterCenters)
+ {
+ double minDistance = double.MaxValue;
+ int closestCluster = -1;
+
+ for (int i = 0; i < clusterCenters.Length; i++)
+ {
+ double distance = GetColorDistance(color, clusterCenters[i]);
+ if (distance < minDistance)
+ {
+ minDistance = distance;
+ closestCluster = i;
+ }
+ }
+
+ return closestCluster;
+ }
+
+ private static double GetColorDistance(SKColor c1, SKColor c2)
+ {
+ return Math.Sqrt(
+ Math.Pow(c1.Red - c2.Red, 2) +
+ Math.Pow(c1.Green - c2.Green, 2) +
+ Math.Pow(c1.Blue - c2.Blue, 2) +
+ Math.Pow(c1.Alpha - c2.Alpha, 2)
+ );
+ }
+
+ private static SKColor GetAverageColor(List colors)
+ {
+ byte averageRed = (byte)colors.Average(c => c.Red);
+ byte averageGreen = (byte)colors.Average(c => c.Green);
+ byte averageBlue = (byte)colors.Average(c => c.Blue);
+ byte averageAlpha = (byte)colors.Average(c => c.Alpha);
+
+ return new SKColor(averageRed, averageGreen, averageBlue, averageAlpha);
+ }
+
+ private static double GetColorBrightness(SKColor color)
+ {
+ return 0.299 * color.Red + 0.587 * color.Green + 0.114 * color.Blue + color.Alpha;
+ }
}
}
\ No newline at end of file
diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs
index 25d289e..ab68445 100644
--- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs
+++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs
@@ -66,7 +66,11 @@ public Pixel Blend(Pixel pixelAbove)
switch (pixelAbove.Background.Mode)
{
case PixelBackgroundMode.Colored:
- return pixelAbove;
+ // merge pixelAbove into this pixel using alpha channel.
+ Color mergedColors = MergeColors(Background.Color, pixelAbove.Background.Color);
+ return new Pixel(pixelAbove.Foreground,
+ new PixelBackground(mergedColors));
+
case PixelBackgroundMode.Transparent:
// when a textdecoration of underline happens a DrawLine() is called over the top of the a pixel with non-zero symbol.
// this detects this situation and eats the draw line, turning it into a textdecoration
@@ -97,5 +101,23 @@ Foreground.Symbol is SimpleSymbol simpleSymbol &&
{
return (Foreground.Shade(), Background.Shade());
}
+
+ ///
+ /// merge colors with alpha blending
+ ///
+ ///
+ ///
+ /// source blended into target
+ private static Color MergeColors(Color target, Color source)
+ {
+ float alphaB = source.A / 255.0f;
+ float inverseAlphaB = 1.0f - alphaB;
+
+ byte red = (byte)(target.R * inverseAlphaB + source.R * alphaB);
+ byte green = (byte)(target.G * inverseAlphaB + source.G * alphaB);
+ byte blue = (byte)(target.B * inverseAlphaB + source.B * alphaB);
+
+ return new Color(0xFF, red, green, blue);
+ }
}
}
\ No newline at end of file
diff --git a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs
index ddefd3a..ac2e8ee 100644
--- a/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs
+++ b/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs
@@ -5,6 +5,12 @@ namespace Consolonia.Core.Drawing.PixelBufferImplementation
{
public readonly struct PixelBackground
{
+ public PixelBackground(Color color)
+ {
+ Mode = color.A == 0 ? PixelBackgroundMode.Transparent : PixelBackgroundMode.Colored;
+ Color = color;
+ }
+
public PixelBackground(PixelBackgroundMode mode, Color? color = null)
{
Color = color ?? Colors.Black;
diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryImage.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryImage.axaml
index d87fa1d..4efd087 100644
--- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryImage.axaml
+++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryImage.axaml
@@ -9,7 +9,7 @@
x:Class="Consolonia.Gallery.Gallery.GalleryViews.GalleryImage">
-
+
diff --git a/src/Consolonia.Gallery/Resources/happy.png b/src/Consolonia.Gallery/Resources/happy.png
new file mode 100644
index 0000000..fc5b126
Binary files /dev/null and b/src/Consolonia.Gallery/Resources/happy.png differ