Is it possible to create a Skia Canvas element in an Avalonia application?
Asked Answered
R

5

8

I am hoping to port an Electron app over to Avalonia. The app currently uses Paper.js to draw and manage interactions with complex polygons. Looking into Avalonia, I noticed it uses Skia, which seems to offer much of the same functionality as Paper.js. I was hoping there would be an easy way of creating a Skia "canvas" and just using the SkiaSharp API directly.

Unfortunately, I'm not having much luck finding documentation/answers. Someone on the avalonia gitter mentioned I might use RenderTargetBitmap, but after looking into the source (can't find any documentation on it) I think it would be easier/more elegant to use a Skia canvas directly.

Is this possible?

Ruddle answered 6/5, 2020 at 4:22 Comment(0)
G
10

You have several options:

  • Use WriteableBitmap: lock the bits, create SKBitmap from it, create SKCanvas, draw. Then use that WritableBitmap as a Source for Image control. This is the safest most portable, but unfortunately least performant way.
  • Access the underlying Skia context directly on the render thread, you can find an example here. Note that Render callback might be called from any thread, so manage your locks properly. Also note that Avalonia has pluggable renderer architecture, so renderer is technically not guaranteed to be Skia, even if it's currently used by default on all platforms and is highly likely to remain the primary renderer
  • Once 0.10 is out, you'll be able to create a hardware-accelerated SKCanvas and render to OpenGL texture. The infrastructure required for using OpenGL textures as Avalonia images is currently being worked on in OpenGL control branch
Gemmation answered 6/5, 2020 at 7:53 Comment(5)
Is there another way to do this? I mean other than using Skia directly. Is there an avalonia control that allows drawing arbitrary polygons? The canvas control looks like it might do the trick but i get the feeling it's more of a layout tool (it is a Panel after all) than a drawing tool (like html canvas).Ruddle
You can just override OnRender and use the DrawingContext if StreamGeometry is sufficient for your needs.Gemmation
@Gemmation I'm trying to SkiaSharp rendering based on your third suggestion but with no success. Can you give an example of how this can be done? I've looked at the demo in the linked PR and tried to combine it with the answer to this question, https://mcmap.net/q/1323346/-skiasharp-draw-to-windowReid
OpenGL rendering is kinda broken for Windows right now unless you are willing to use WGL instead of ANGLE, so the second option currently provides the best performance. We are planning to provide a built-in control for SkiaSharp rendering with multiple options Q1 or Q2 this year.Gemmation
@Gemmation I'm evaluating Avalonia, is the built-in control for SkiaSharp rendering with multiple options ready?Kinard
R
4

I had trouble figuring this out using other answers on this site, so hopefully this helps others.

The idea is that any control has an overridable Render method. This method can call context.Custom which takes an ICustomDrawOperation instance. My understanding is that Skia rendering is not guaranteed, so the code needs to check and cast to a ISkiaDrawingContextImpl.

For those who just want to drop something in (like I did), here's some code that should help:

public partial class SkiaCanvas : UserControl
{
    class RenderingLogic : ICustomDrawOperation
    {
        public Action<SKCanvas> RenderCall;
        public Rect Bounds { get; set; }

        public void Dispose() {}

        public bool Equals(ICustomDrawOperation? other) => other == this;

        // not sure what goes here....
        public bool HitTest(Point p) { return false; }

        public void Render(IDrawingContextImpl context)
        {
            var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
            if(canvas != null)
            {
                Render(canvas);
            }
        }

        private void Render(SKCanvas canvas)
        {
            RenderCall?.Invoke(canvas);
        }
    }

    RenderingLogic renderingLogic;

    public event Action<SKCanvas> RenderSkia;

    public SkiaCanvas()
    {
        InitializeComponent();

        renderingLogic = new RenderingLogic();
        renderingLogic.RenderCall += (canvas) => RenderSkia?.Invoke(canvas);
    }

    public override void Render(DrawingContext context)
    {
        renderingLogic.Bounds = new Rect(0, 0, this.Bounds.Width, this.Bounds.Height);

        context.Custom(renderingLogic);
        
        // If you want continual invalidation (like a game):
        //Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
    }
}

Now you can drop the skia canvas in your own AXAML:

<views:SkiaCanvas Height="200" RenderSkia="HandleRenderSkia"></views:SkiaCanvas>

Implement the HandleRenderSkia in your codebehind:

void HandleRenderSkia(SKCanvas canvas)
{
    canvas.DrawCircle(100, 100, 80, new SKPaint() { Color =  SKColors.Green, IsAntialias = true });
}

Now you'll have full skia rendering:

enter image description here

Reimers answered 26/9, 2022 at 17:38 Comment(1)
The official Avalonia sample using Skia that this was based on is: github.com/AvaloniaUI/Avalonia/blob/master/samples/RenderDemo/…Fiddlewood
O
3

Thank you Victor, it helped a lot with Avalonia 0.10.18

Since Avalonia 11.0.0 preview3 a feature approach helps getting the SkCanvas like this:

public void Render(IDrawingContextImpl context) {
    // Avalonia 0.10.18 method
    //var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
    //if (canvas != null) RenderAction?.Invoke(canvas);

    // Avalonia 11.0.0 preview feature method
    var skia = context.GetFeature<ISkiaSharpApiLeaseFeature>();
    using (var lease = skia.Lease()) {
        SKCanvas canvas = lease.SkCanvas;
        if (canvas != null) RenderAction?.Invoke(canvas);
    }
}

Another issue came with resizing the window where presumably the skia bitmap buffer was not sufficient to cover the larger window space. Following solution worked just fine:

public SkiaCanvas()
{
    InitializeComponent();

    // create RenderingLogic when rendering
}

public override void Render(DrawingContext context)
{
    if (renderingLogic == null || renderingLogic.Bounds != this.Bounds) {
        // (re)create drawing operation matching actual bounds
        if (renderingLogic != null) renderingLogic.Dispose();
        renderingLogic = new SkiaDrawOperation();
        renderingLogic.RenderAction += (canvas) => OnSkiaRendering(canvas);
        renderingLogic.Bounds = new Rect(0, 0, this.Bounds.Width, this.Bounds.Height);
    }

    renderingLogic.Bounds = new Rect(0, 0, this.Bounds.Width, this.Bounds.Height);

    context.Custom(renderingLogic);

    // ...
}
Ortrud answered 5/11, 2022 at 0:6 Comment(0)
T
2

Here is a mashup of the answers provided by Victor Chelaru and friedrich using Avalonia 11.0.0-preview7. I added it here because as a new user to Avalonia I had some trouble getting it all working.

I have not included the extra resizing code as I personally don't require it.

In your solution you will need to add the "Avalonia.Skia" NuGet package.

using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using SkiaSharp;
using System;

namespace YOUR_NAMESPACE_GOES_HERE;

public partial class SkiaCanvas : UserControl
{
    class RenderingLogic : ICustomDrawOperation
    {
        public Action<SKCanvas> RenderCall;
        public Rect Bounds { get; set; }

        public void Dispose() { }

        public bool Equals(ICustomDrawOperation? other) => other == this;

        // not sure what goes here....
        public bool HitTest(Point p) { return false; }

        public void Render(IDrawingContextImpl context)
        {
            var skia = context.GetFeature<ISkiaSharpApiLeaseFeature>();
            using (var lease = skia.Lease())
            {
                SKCanvas canvas = lease.SkCanvas;
                if (canvas != null) RenderCall?.Invoke(canvas);
            }
        }
    }

    RenderingLogic renderingLogic;

    public event Action<SKCanvas> RenderSkia;

    public SkiaCanvas(int width, int height)
    {
        InitializeComponent();

        Width = width;
        Height = height;
        Bounds = new Rect(0, 0, width, height);

        HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left;
        VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top;

        Initialized += SkiaCanvas_Initialized;

        renderingLogic = new RenderingLogic();
        renderingLogic.RenderCall += (canvas) => RenderSkia?.Invoke(canvas);
    }

    private void SkiaCanvas_Initialized(object? sender, EventArgs e)
    {
        // Remove this if you don't need to do anything when this event is raised.
    }

    public override void Render(DrawingContext context)
    {
        renderingLogic.Bounds = new Rect(0, 0, this.Bounds.Width, this.Bounds.Height);
        context.Custom((ICustomDrawOperation)renderingLogic);
    }
}
Theatrics answered 5/5, 2023 at 8:19 Comment(0)
D
0

In case it helps anyone who finds themselves here because they're trying to convert SkiaSharp SKBitmap to an Avalonia Bitmap. You can do it like this:

public Avalonia.Media.Imaging.Bitmap SKBitmapToAvaloniaBitmap(SKBitmap skBitmap)
{
    SKData data = skBitmap.Encode(SKEncodedImageFormat.Png, 100);
    using (Stream stream = data.AsStream())
    {
        return new Avalonia.Media.Imaging.Bitmap(stream);
    }
}
Deettadeeyn answered 2/9, 2023 at 6:58 Comment(1)
is it posible get SKBitmap from Avalonia.Media.Imaging.Bitmap?Ashburn

© 2022 - 2025 — McMap. All rights reserved.