From 4d0605dbb116e699b22d500fc787664c433f955a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 19 Jun 2016 19:56:49 -0400 Subject: [PATCH] Added in code that somewhat works for Skia opacity masks. Test is still skipped because it doesn't fully work yet. --- .../Avalonia.Cairo/Media/DrawingContext.cs | 100 +++++++++--------- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 83 +++++++++++---- .../Avalonia.RenderTests/OpacityMaskTests.cs | 2 +- 3 files changed, 113 insertions(+), 72 deletions(-) diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs index c2f703918d..696274ed69 100644 --- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs @@ -85,31 +85,31 @@ namespace Avalonia.Cairo.Media _context.Scale(scale.X, scale.Y); destRect /= scale; - if (opacityOverride < 1.0f) { - _context.PushGroup (); - Gdk.CairoHelper.SetSourcePixbuf ( - _context, - impl.Surface, - -sourceRect.X + destRect.X, - -sourceRect.Y + destRect.Y); - - _context.Rectangle (destRect.ToCairo ()); - _context.Fill (); - _context.PopGroupToSource (); - _context.PaintWithAlpha (opacityOverride); - } else { - _context.PushGroup (); - Gdk.CairoHelper.SetSourcePixbuf ( - _context, - impl.Surface, - -sourceRect.X + destRect.X, - -sourceRect.Y + destRect.Y); - - _context.Rectangle (destRect.ToCairo ()); - _context.Fill (); - _context.PopGroupToSource (); - _context.PaintWithAlpha (opacityOverride); - } + if (opacityOverride < 1.0f) { + _context.PushGroup (); + Gdk.CairoHelper.SetSourcePixbuf ( + _context, + impl.Surface, + -sourceRect.X + destRect.X, + -sourceRect.Y + destRect.Y); + + _context.Rectangle (destRect.ToCairo ()); + _context.Fill (); + _context.PopGroupToSource (); + _context.PaintWithAlpha (opacityOverride); + } else { + _context.PushGroup (); + Gdk.CairoHelper.SetSourcePixbuf ( + _context, + impl.Surface, + -sourceRect.X + destRect.X, + -sourceRect.Y + destRect.Y); + + _context.Rectangle (destRect.ToCairo ()); + _context.Fill (); + _context.PopGroupToSource (); + _context.PaintWithAlpha (opacityOverride); + } _context.Restore(); } @@ -123,12 +123,12 @@ namespace Avalonia.Cairo.Media { var size = new Rect(p1, p2).Size; - using (var p = SetPen(pen, size)) - { - _context.MoveTo(p1.ToCairo()); - _context.LineTo(p2.ToCairo()); - _context.Stroke(); - } + using (var p = SetPen(pen, size)) + { + _context.MoveTo(p1.ToCairo()); + _context.LineTo(p2.ToCairo()); + _context.Stroke(); + } } /// @@ -179,11 +179,11 @@ namespace Avalonia.Cairo.Media /// The rectangle bounds. public void DrawRectangle(Pen pen, Rect rect, float cornerRadius) { - using (var p = SetPen(pen, rect.Size)) - { - _context.Rectangle(rect.ToCairo ()); - _context.Stroke(); - } + using (var p = SetPen(pen, rect.Size)) + { + _context.Rectangle(rect.ToCairo ()); + _context.Stroke(); + } } /// @@ -197,10 +197,10 @@ namespace Avalonia.Cairo.Media var layout = ((FormattedTextImpl)text.PlatformImpl).Layout; _context.MoveTo(origin.X, origin.Y); - using (var b = SetBrush(foreground, new Size(0, 0))) - { - Pango.CairoHelper.ShowLayout(_context, layout); - } + using (var b = SetBrush(foreground, new Size(0, 0))) + { + Pango.CairoHelper.ShowLayout(_context, layout); + } } /// @@ -210,11 +210,11 @@ namespace Avalonia.Cairo.Media /// The rectangle bounds. public void FillRectangle(IBrush brush, Rect rect, float cornerRadius) { - using (var b = SetBrush(brush, rect.Size)) - { - _context.Rectangle(rect.ToCairo ()); - _context.Fill(); - } + using (var b = SetBrush(brush, rect.Size)) + { + _context.Rectangle(rect.ToCairo ()); + _context.Fill(); + } } /// @@ -271,7 +271,7 @@ namespace Avalonia.Cairo.Media }); } - private double opacityOverride = 1.0f; + private double opacityOverride = 1.0f; private IDisposable SetBrush(IBrush brush, Size destinationSize) { @@ -344,10 +344,10 @@ namespace Avalonia.Cairo.Media _context.LineJoin = Cairo.LineJoin.Miter; _context.LineCap = Cairo.LineCap.Butt; - if (pen.Brush == null) - return Disposable.Empty; - - return SetBrush(pen.Brush, destinationSize); + if (pen.Brush == null) + return Disposable.Empty; + + return SetBrush(pen.Brush, destinationSize); } public void PushGeometryClip(Geometry clip) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 781ea4b4df..4ae306ff16 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,12 +10,16 @@ namespace Avalonia.Skia { internal class DrawingContextImpl : IDrawingContextImpl { - public SKCanvas Canvas { get; private set; } + private Stack surfaceStack = new Stack(); + private Stack maskStack = new Stack(); + private SKCanvas initialCanvas; + + public SKCanvas CurrentCanvas => surfaceStack.Count == 0 ? initialCanvas : surfaceStack.Peek().Canvas; public DrawingContextImpl(SKCanvas canvas) { - Canvas = canvas; - Canvas.Clear(); + initialCanvas = canvas; + initialCanvas.Clear(); } public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) @@ -23,14 +27,14 @@ namespace Avalonia.Skia var impl = (BitmapImpl)source.PlatformImpl; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); - Canvas.DrawBitmap(impl.Bitmap, s, d); + CurrentCanvas.DrawBitmap(impl.Bitmap, s, d); } public void DrawLine(Pen pen, Point p1, Point p2) { using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { - Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); + CurrentCanvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); } } @@ -44,15 +48,27 @@ namespace Avalonia.Skia { if (fill.Paint != null) { - Canvas.DrawPath(impl.EffectivePath, fill.Paint); + CurrentCanvas.DrawPath(impl.EffectivePath, fill.Paint); } if (stroke.Paint != null) { - Canvas.DrawPath(impl.EffectivePath, stroke.Paint); + CurrentCanvas.DrawPath(impl.EffectivePath, stroke.Paint); } } } + struct MaskWrapper : IDisposable + { + public PaintWrapper Mask { get; set; } + public Rect Bounds { get; set; } + + public void Dispose() + { + Mask.Dispose(); + } + } + + struct PaintWrapper : IDisposable { //We are saving memory allocations there @@ -225,11 +241,11 @@ namespace Avalonia.Skia var rc = rect.ToSKRect(); if (cornerRadius == 0) { - Canvas.DrawRect(rc, paint.Paint); + CurrentCanvas.DrawRect(rc, paint.Paint); } else { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + CurrentCanvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); } } } @@ -241,11 +257,11 @@ namespace Avalonia.Skia var rc = rect.ToSKRect(); if (cornerRadius == 0) { - Canvas.DrawRect(rc, paint.Paint); + CurrentCanvas.DrawRect(rc, paint.Paint); } else { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + CurrentCanvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); } } } @@ -255,19 +271,19 @@ namespace Avalonia.Skia using (var paint = CreatePaint(foreground, text.Measure())) { var textImpl = text.PlatformImpl as FormattedTextImpl; - textImpl.Draw(this.Canvas, origin.ToSKPoint()); + textImpl.Draw(this.CurrentCanvas, origin.ToSKPoint()); } } public void PushClip(Rect clip) { - Canvas.Save(); - Canvas.ClipRect(clip.ToSKRect()); + CurrentCanvas.Save(); + CurrentCanvas.ClipRect(clip.ToSKRect()); } public void PopClip() { - Canvas.Restore(); + CurrentCanvas.Restore(); } double _currentOpacity = 1.0f; @@ -290,23 +306,48 @@ namespace Avalonia.Skia public void PushGeometryClip(Geometry clip) { - Canvas.Save(); - Canvas.ClipPath(((StreamGeometryImpl)clip.PlatformImpl).EffectivePath); + CurrentCanvas.Save(); + CurrentCanvas.ClipPath(((StreamGeometryImpl)clip.PlatformImpl).EffectivePath); } public void PopGeometryClip() { - Canvas.Restore(); + CurrentCanvas.Restore(); } public void PushOpacityMask(IBrush mask, Rect bounds) { - //TODO: Skia does not support opacity masks + surfaceStack.Push(SKSurface.Create((int)bounds.Width, (int)bounds.Height, SKColorType.N_32, SKAlphaType.Premul)); + surfaceStack.Peek().Canvas.Clear(); + var paint = new MaskWrapper { Mask = CreatePaint(mask, bounds.Size), Bounds = bounds }; + maskStack.Push(paint); } public void PopOpacityMask() { - //TODO: Skia does not support opacity masks + using (var surface = surfaceStack.Pop()) + using (var mask = maskStack.Pop()) + using (var combindingPaint = new SKPaint()) + using (var surfaceImage = surface.Snapshot()) + { + using (var maskSurface = SKSurface.Create((int)mask.Bounds.Width, (int)mask.Bounds.Height, SKColorType.N_32, SKAlphaType.Premul)) + { + maskSurface.Canvas.Clear(SKColors.Transparent); + maskSurface.Canvas.DrawRect(SKRect.Create((float)mask.Bounds.Width, (float)mask.Bounds.Height), mask.Mask.Paint); + using (var maskImage = maskSurface.Snapshot()) + using (var combindingSurface = SKSurface.Create((int)mask.Bounds.Width, (int)mask.Bounds.Height, SKColorType.N_32, SKAlphaType.Premul)) + { + combindingSurface.Canvas.Clear(SKColors.Transparent); + combindingSurface.Canvas.DrawImage(surfaceImage, 0, 0, combindingPaint); + combindingPaint.XferMode = SKXferMode.DstIn; + combindingSurface.Canvas.DrawImage(maskImage, 0, 0, combindingPaint); + using (var maskedImage = combindingSurface.Snapshot()) + { + CurrentCanvas.DrawImage(maskedImage, mask.Bounds.ToSKRect()); + } + } + } + } } private Matrix _currentTransform = Matrix.Identity; @@ -320,7 +361,7 @@ namespace Avalonia.Skia return; _currentTransform = value; - Canvas.SetMatrix(value.ToSKMatrix()); + CurrentCanvas.SetMatrix(value.ToSKMatrix()); } } } diff --git a/tests/Avalonia.RenderTests/OpacityMaskTests.cs b/tests/Avalonia.RenderTests/OpacityMaskTests.cs index 7d9b6d7fa8..903181c763 100644 --- a/tests/Avalonia.RenderTests/OpacityMaskTests.cs +++ b/tests/Avalonia.RenderTests/OpacityMaskTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.RenderTests } #if AVALONIA_SKIA - [Fact(Skip = "Opacity Masks not supported on Skia")] + [Fact(Skip = "Opacity Masks on Skia are currently bugged.")] #else [Fact] #endif