Skip to content

Commit

Permalink
support transparent background for images with blending into the pixe…
Browse files Browse the repository at this point in the history
…lbuffer background.
  • Loading branch information
tomlm committed Nov 4, 2024
1 parent 6db80a9 commit 37d4494
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 87 deletions.
199 changes: 114 additions & 85 deletions src/Consolonia.Core/Drawing/DrawingContextImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,41 +68,36 @@ public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect
new SKPaint { FilterQuality = SKFilterQuality.Medium });

for (int y = 0; y < bitmap.Info.Height; y += 2)
for (int x = 0; x < bitmap.Info.Width; x += 2)
{
// 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[]
for (int x = 0; x < bitmap.Info.Width; x += 2)
{
bitmap.GetPixel(x, y), bitmap.GetPixel(x + 1, y),
bitmap.GetPixel(x, y + 1), bitmap.GetPixel(x + 1, y + 1)
};
// 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;

// map it to a single char to represet the 4 pixels
char quadPixel = GetQuadPixelCharacter(quadColors);

// get the combined colors for the quad pixel
var quadPixels = new[]
{
bitmap.GetPixel(x, y), bitmap.GetPixel(x + 1, y),
bitmap.GetPixel(x, y + 1), bitmap.GetPixel(x + 1, y + 1)
};
Color foreground = GetForegroundColorForQuadPixel(quadPixels, quadPixel);
Color background = GetBackgroundColorForQuadPixel(quadPixels, quadPixel);

var imagePixel = new Pixel(new PixelForeground(new SimpleSymbol(quadPixel), color: foreground),
new PixelBackground(PixelBackgroundMode.Colored, background));
CurrentClip.ExecuteWithClipping(new Point(px, py),
() =>
// get the quad pixel the bitmap
var quadColors = new[]
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(existingPixel, _) => existingPixel.Blend(imagePixel), imagePixel.Background.Color);
});
}
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),
() =>
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(existingPixel, _) => existingPixel.Blend(imagePixel), imagePixel.Background.Color);
});
}
}

public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
Expand Down Expand Up @@ -153,31 +148,31 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
case VisualBrush:
throw new NotImplementedException();
case ISceneBrush sceneBrush:
{
ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent();
if (sceneBrushContent != null) sceneBrushContent.Render(this, Matrix.Identity);
return;
}
{
ISceneBrushContent sceneBrushContent = sceneBrush.CreateContent();
if (sceneBrushContent != null) sceneBrushContent.Render(this, Matrix.Identity);
return;
}
}

Rect r2 = r.TransformToAABB(Transform);

double width = r2.Width + (pen?.Thickness ?? 0);
double height = r2.Height + (pen?.Thickness ?? 0);
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
int px = (int)(r2.TopLeft.X + x);
int py = (int)(r2.TopLeft.Y + y);

ConsoleBrush backgroundBrush = ConsoleBrush.FromPosition(brush, x, y, (int)width, (int)height);
CurrentClip.ExecuteWithClipping(new Point(px, py), () =>
for (int y = 0; y < height; y++)
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(pixel, bb) => pixel.Blend(new Pixel(new PixelBackground(bb.Mode, bb.Color))),
backgroundBrush);
});
}
int px = (int)(r2.TopLeft.X + x);
int py = (int)(r2.TopLeft.Y + y);

ConsoleBrush backgroundBrush = ConsoleBrush.FromPosition(brush, x, y, (int)width, (int)height);
CurrentClip.ExecuteWithClipping(new Point(px, py), () =>
{
_pixelBuffer.Set(new PixelBufferCoordinate((ushort)px, (ushort)py),
(pixel, bb) => pixel.Blend(new Pixel(new PixelBackground(bb.Mode, bb.Color))),
backgroundBrush);
});
}
}

if (pen is null or { Thickness: 0 }
Expand Down Expand Up @@ -475,43 +470,43 @@ private void DrawPixelAndMoveHead(ref Point head, Line line, LineStyle? lineStyl
switch (c)
{
case '\t':
{
const int tabSize = 8;
var consolePixel = new Pixel(' ', foregroundColor);
for (int j = 0; j < tabSize; j++)
{
Point newCharacterPoint = characterPoint.WithX(characterPoint.X + j);
CurrentClip.ExecuteWithClipping(newCharacterPoint, () =>
const int tabSize = 8;
var consolePixel = new Pixel(' ', foregroundColor);
for (int j = 0; j < tabSize; j++)
{
_pixelBuffer.Set((PixelBufferCoordinate)newCharacterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});
Point newCharacterPoint = characterPoint.WithX(characterPoint.X + j);
CurrentClip.ExecuteWithClipping(newCharacterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)newCharacterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});
}

currentXPosition += tabSize - 1;
}

currentXPosition += tabSize - 1;
}
break;
case '\n':
{
/* it's not clear if we need to draw anything. Cursor can be placed at the end of the line
var consolePixel = new Pixel(' ', foregroundColor);
{
/* it's not clear if we need to draw anything. Cursor can be placed at the end of the line
var consolePixel = new Pixel(' ', foregroundColor);
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);*/
}
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);*/
}
break;
case '\u200B':
currentXPosition--;
break;
default:
{
var consolePixel = new Pixel(c, foregroundColor, typeface.Style, typeface.Weight);
CurrentClip.ExecuteWithClipping(characterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});
}
var consolePixel = new Pixel(c, foregroundColor, typeface.Style, typeface.Weight);
CurrentClip.ExecuteWithClipping(characterPoint, () =>
{
_pixelBuffer.Set((PixelBufferCoordinate)characterPoint,
(oldPixel, cp) => oldPixel.Blend(cp), consolePixel);
});
}
break;
}
}
Expand Down Expand Up @@ -615,13 +610,39 @@ private static Color GetBackgroundColorForQuadPixel(SKColor[] pixelColors, char
'█' => SKColors.Transparent,
_ => throw new NotImplementedException()
};
return Color.FromRgb(skColor.Red, skColor.Green, skColor.Blue);
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)
{
return new SKColor((byte)colors.Average(c => c.Red), (byte)colors.Average(c => c.Green),
(byte)colors.Average(c => c.Blue));
float finalRed = 0;
float finalGreen = 0;
float finalBlue = 0;
float finalAlpha = 0;

foreach (var 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);
}

/// <summary>
Expand All @@ -648,8 +669,14 @@ private static string GetColorsPattern(SKColor[] colors)
for (int cluster = 0; cluster < 2; cluster++)
{
var clusteredColors = colors.Where((_, i) => clusters[i] == cluster).ToList();
newClusterCenters[cluster] = GetAverageColor(clusteredColors);
if (clusteredColors.Count == 4) return "TTTT";
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
Expand Down Expand Up @@ -693,22 +720,24 @@ 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.Blue - c2.Blue, 2) +
Math.Pow(c1.Alpha - c2.Alpha, 2)
);
}

private static SKColor GetAverageColor(List<SKColor> 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);
var averageRed = (byte)colors.Average(c => c.Red);
var averageGreen = (byte)colors.Average(c => c.Green);
var averageBlue = (byte)colors.Average(c => c.Blue);
var averageAlpha = (byte)colors.Average(c => c.Alpha);

return new SKColor(averageRed, averageGreen, averageBlue);
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;
return 0.299 * color.Red + 0.587 * color.Green + 0.114 * color.Blue + color.Alpha;
}
}
}
24 changes: 23 additions & 1 deletion src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
var 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
Expand Down Expand Up @@ -97,5 +101,23 @@ Foreground.Symbol is SimpleSymbol simpleSymbol &&
{
return (Foreground.Shade(), Background.Shade());
}

/// <summary>
/// merge colors with alpha blending
/// </summary>
/// <param name="target"></param>
/// <param name="source"></param>
/// <returns>source blended into target</returns>
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));

Check notice on line 116 in src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs

View workflow job for this annotation

GitHub Actions / build

"[ArrangeRedundantParentheses] Redundant parentheses" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs(116,31)

Check notice on line 116 in src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs

View workflow job for this annotation

GitHub Actions / build

"[ArrangeRedundantParentheses] Redundant parentheses" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs(116,60)
byte green = (byte)((target.G * inverseAlphaB) + (source.G * alphaB));

Check notice on line 117 in src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs

View workflow job for this annotation

GitHub Actions / build

"[ArrangeRedundantParentheses] Redundant parentheses" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs(117,33)

Check notice on line 117 in src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs

View workflow job for this annotation

GitHub Actions / build

"[ArrangeRedundantParentheses] Redundant parentheses" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs(117,62)
byte blue = (byte)((target.B * inverseAlphaB) + (source.B * alphaB));

Check notice on line 118 in src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs

View workflow job for this annotation

GitHub Actions / build

"[ArrangeRedundantParentheses] Redundant parentheses" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs(118,32)

Check notice on line 118 in src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs

View workflow job for this annotation

GitHub Actions / build

"[ArrangeRedundantParentheses] Redundant parentheses" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/PixelBufferImplementation/Pixel.cs(118,61)

return new Color(0xFF, red, green, blue);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Check notice on line 10 in src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs

View workflow job for this annotation

GitHub Actions / build

"[ArrangeRedundantParentheses] Redundant parentheses" on /home/runner/work/Consolonia/Consolonia/src/Consolonia.Core/Drawing/PixelBufferImplementation/PixelBackground.cs(10,20)
Color = color;
}

public PixelBackground(PixelBackgroundMode mode, Color? color = null)
{
Color = color ?? Colors.Black;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
x:Class="Consolonia.Gallery.Gallery.GalleryViews.GalleryImage">

<Grid RowDefinitions="20 *">
<Image Source="avares://Consolonia.Gallery/Resources/happy.jpg" Margin="0 1 1 0" />
<Image Source="avares://Consolonia.Gallery/Resources/happy.png" Margin="0 1 1 0" />

<Border BorderBrush="Gray" BorderThickness="1" Grid.Row="1">
<ScrollViewer >
Expand Down
Binary file added src/Consolonia.Gallery/Resources/happy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 37d4494

Please sign in to comment.