diff --git a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs index 384a3691..ca281eaf 100644 --- a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs +++ b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs @@ -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) @@ -153,11 +148,11 @@ 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); @@ -165,19 +160,19 @@ public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) 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 } @@ -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; } } @@ -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); } /// @@ -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 @@ -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 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; } } } \ 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 25d289e0..8fc455ab 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. + 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 @@ -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 ddefd3af..8d528c1e 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 d87fa1db..4efd0871 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 00000000..fc5b126f Binary files /dev/null and b/src/Consolonia.Gallery/Resources/happy.png differ