|
@@ -1,57 +1,114 @@
|
|
|
-using Avalonia.Media;
|
|
|
-using SkiaSharp;
|
|
|
+// Copyright (c) The Avalonia Project. All rights reserved.
|
|
|
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
+
|
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
+using System.Diagnostics;
|
|
|
using System.Linq;
|
|
|
+using Avalonia.Media;
|
|
|
using Avalonia.Platform;
|
|
|
using Avalonia.Rendering;
|
|
|
using Avalonia.Rendering.Utilities;
|
|
|
using Avalonia.Utilities;
|
|
|
+using SkiaSharp;
|
|
|
|
|
|
namespace Avalonia.Skia
|
|
|
{
|
|
|
- internal class DrawingContextImpl : IDrawingContextImpl
|
|
|
+ /// <summary>
|
|
|
+ /// Skia based drawing context.
|
|
|
+ /// </summary>
|
|
|
+ public class DrawingContextImpl : IDrawingContextImpl
|
|
|
{
|
|
|
+ private readonly IDisposable[] _disposables;
|
|
|
private readonly Vector _dpi;
|
|
|
+ private readonly Stack<PaintWrapper> _maskStack = new Stack<PaintWrapper>();
|
|
|
+ private readonly Stack<double> _opacityStack = new Stack<double>();
|
|
|
private readonly Matrix? _postTransform;
|
|
|
- private readonly IDisposable[] _disposables;
|
|
|
private readonly IVisualBrushRenderer _visualBrushRenderer;
|
|
|
- private Stack<PaintWrapper> maskStack = new Stack<PaintWrapper>();
|
|
|
- protected bool CanUseLcdRendering = true;
|
|
|
- public SKCanvas Canvas { get; private set; }
|
|
|
-
|
|
|
- public DrawingContextImpl(
|
|
|
- SKCanvas canvas,
|
|
|
- Vector dpi,
|
|
|
- IVisualBrushRenderer visualBrushRenderer,
|
|
|
- params IDisposable[] disposables)
|
|
|
+ private double _currentOpacity = 1.0f;
|
|
|
+ private readonly bool _canTextUseLcdRendering;
|
|
|
+ private Matrix _currentTransform;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Context create info.
|
|
|
+ /// </summary>
|
|
|
+ public struct CreateInfo
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Canvas to draw to.
|
|
|
+ /// </summary>
|
|
|
+ public SKCanvas Canvas;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Dpi of drawings.
|
|
|
+ /// </summary>
|
|
|
+ public Vector Dpi;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Visual brush renderer.
|
|
|
+ /// </summary>
|
|
|
+ public IVisualBrushRenderer VisualBrushRenderer;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Render text without Lcd rendering.
|
|
|
+ /// </summary>
|
|
|
+ public bool DisableTextLcdRendering;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Create new drawing context.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="createInfo">Create info.</param>
|
|
|
+ /// <param name="disposables">Array of elements to dispose after drawing has finished.</param>
|
|
|
+ public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables)
|
|
|
{
|
|
|
- _dpi = dpi;
|
|
|
- if (dpi.X != 96 || dpi.Y != 96)
|
|
|
- _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96);
|
|
|
- _visualBrushRenderer = visualBrushRenderer;
|
|
|
+ _dpi = createInfo.Dpi;
|
|
|
+ _visualBrushRenderer = createInfo.VisualBrushRenderer;
|
|
|
_disposables = disposables;
|
|
|
- Canvas = canvas;
|
|
|
+ _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering;
|
|
|
+
|
|
|
+ Canvas = createInfo.Canvas;
|
|
|
+
|
|
|
+ if (Canvas == null)
|
|
|
+ {
|
|
|
+ throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
|
|
|
+ {
|
|
|
+ _postTransform =
|
|
|
+ Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
|
|
|
+ }
|
|
|
+
|
|
|
Transform = Matrix.Identity;
|
|
|
}
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Skia canvas.
|
|
|
+ /// </summary>
|
|
|
+ public SKCanvas Canvas { get; }
|
|
|
|
|
|
+ /// <inheritdoc />
|
|
|
public void Clear(Color color)
|
|
|
{
|
|
|
Canvas.Clear(color.ToSKColor());
|
|
|
}
|
|
|
|
|
|
+ /// <inheritdoc />
|
|
|
public void DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
|
|
|
{
|
|
|
- var impl = (BitmapImpl)source.Item;
|
|
|
+ var drawableImage = (IDrawableBitmapImpl) source.Item;
|
|
|
var s = sourceRect.ToSKRect();
|
|
|
var d = destRect.ToSKRect();
|
|
|
- using (var paint = new SKPaint()
|
|
|
- { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) })
|
|
|
+
|
|
|
+ using (var paint =
|
|
|
+ new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))})
|
|
|
{
|
|
|
- Canvas.DrawBitmap(impl.Bitmap, s, d, paint);
|
|
|
+ drawableImage.Draw(this, s, d, paint);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// <inheritdoc />
|
|
|
public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
|
|
|
{
|
|
|
PushOpacityMask(opacityMask, opacityMaskRect);
|
|
@@ -59,17 +116,19 @@ namespace Avalonia.Skia
|
|
|
PopOpacityMask();
|
|
|
}
|
|
|
|
|
|
+ /// <inheritdoc />
|
|
|
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);
|
|
|
+ Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// <inheritdoc />
|
|
|
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
|
|
|
{
|
|
|
- var impl = (GeometryImpl)geometry;
|
|
|
+ var impl = (GeometryImpl) geometry;
|
|
|
var size = geometry.Bounds.Size;
|
|
|
|
|
|
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
|
|
@@ -79,6 +138,7 @@ namespace Avalonia.Skia
|
|
|
{
|
|
|
Canvas.DrawPath(impl.EffectivePath, fill.Paint);
|
|
|
}
|
|
|
+
|
|
|
if (stroke.Paint != null)
|
|
|
{
|
|
|
Canvas.DrawPath(impl.EffectivePath, stroke.Paint);
|
|
@@ -86,227 +146,424 @@ namespace Avalonia.Skia
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private struct PaintState : IDisposable
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
|
|
|
{
|
|
|
- private readonly SKColor _color;
|
|
|
- private readonly SKShader _shader;
|
|
|
- private readonly SKPaint _paint;
|
|
|
+ using (var paint = CreatePaint(pen, rect.Size))
|
|
|
+ {
|
|
|
+ var rc = rect.ToSKRect();
|
|
|
|
|
|
- public PaintState(SKPaint paint, SKColor color, SKShader shader)
|
|
|
+ if (Math.Abs(cornerRadius) < float.Epsilon)
|
|
|
+ {
|
|
|
+ Canvas.DrawRect(rc, paint.Paint);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
|
|
|
+ {
|
|
|
+ using (var paint = CreatePaint(brush, rect.Size))
|
|
|
{
|
|
|
- _paint = paint;
|
|
|
- _color = color;
|
|
|
- _shader = shader;
|
|
|
+ var rc = rect.ToSKRect();
|
|
|
+
|
|
|
+ if (Math.Abs(cornerRadius) < float.Epsilon)
|
|
|
+ {
|
|
|
+ Canvas.DrawRect(rc, paint.Paint);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- public void Dispose()
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
|
|
|
+ {
|
|
|
+ using (var paint = CreatePaint(foreground, text.Size))
|
|
|
{
|
|
|
- _paint.Color = _color;
|
|
|
- _paint.Shader = _shader;
|
|
|
+ var textImpl = (FormattedTextImpl) text;
|
|
|
+ textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- internal struct PaintWrapper : IDisposable
|
|
|
+ /// <inheritdoc />
|
|
|
+ public IRenderTargetBitmapImpl CreateLayer(Size size)
|
|
|
{
|
|
|
- //We are saving memory allocations there
|
|
|
- //TODO: add more disposable fields if needed
|
|
|
- public readonly SKPaint Paint;
|
|
|
+ var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
|
|
|
+ var pixelSize = size * normalizedDpi;
|
|
|
|
|
|
- private IDisposable _disposable1;
|
|
|
- private IDisposable _disposable2;
|
|
|
+ return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi);
|
|
|
+ }
|
|
|
|
|
|
- public IDisposable ApplyTo(SKPaint paint)
|
|
|
- {
|
|
|
- var state = new PaintState(paint, paint.Color, paint.Shader);
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PushClip(Rect clip)
|
|
|
+ {
|
|
|
+ Canvas.Save();
|
|
|
+ Canvas.ClipRect(clip.ToSKRect());
|
|
|
+ }
|
|
|
|
|
|
- paint.Color = Paint.Color;
|
|
|
- paint.Shader = Paint.Shader;
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PopClip()
|
|
|
+ {
|
|
|
+ Canvas.Restore();
|
|
|
+ }
|
|
|
|
|
|
- return state;
|
|
|
- }
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PushOpacity(double opacity)
|
|
|
+ {
|
|
|
+ _opacityStack.Push(_currentOpacity);
|
|
|
+ _currentOpacity *= opacity;
|
|
|
+ }
|
|
|
|
|
|
- public void AddDisposable(IDisposable disposable)
|
|
|
- {
|
|
|
- if (_disposable1 == null)
|
|
|
- _disposable1 = disposable;
|
|
|
- else if (_disposable2 == null)
|
|
|
- _disposable2 = disposable;
|
|
|
- else
|
|
|
- throw new InvalidOperationException();
|
|
|
- }
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PopOpacity()
|
|
|
+ {
|
|
|
+ _currentOpacity = _opacityStack.Pop();
|
|
|
+ }
|
|
|
|
|
|
- public PaintWrapper(SKPaint paint)
|
|
|
+ /// <inheritdoc />
|
|
|
+ public virtual void Dispose()
|
|
|
+ {
|
|
|
+ if (_disposables == null)
|
|
|
{
|
|
|
- Paint = paint;
|
|
|
- _disposable1 = null;
|
|
|
- _disposable2 = null;
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- public void Dispose()
|
|
|
+ foreach (var disposable in _disposables)
|
|
|
{
|
|
|
- Paint?.Dispose();
|
|
|
- _disposable1?.Dispose();
|
|
|
- _disposable2?.Dispose();
|
|
|
+ disposable?.Dispose();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PushGeometryClip(IGeometryImpl clip)
|
|
|
{
|
|
|
- SKPaint paint = new SKPaint();
|
|
|
- var rv = new PaintWrapper(paint);
|
|
|
- paint.IsStroke = false;
|
|
|
+ Canvas.Save();
|
|
|
+ Canvas.ClipPath(((GeometryImpl)clip).EffectivePath);
|
|
|
+ }
|
|
|
|
|
|
-
|
|
|
- double opacity = brush.Opacity * _currentOpacity;
|
|
|
- paint.IsAntialias = true;
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PopGeometryClip()
|
|
|
+ {
|
|
|
+ Canvas.Restore();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PushOpacityMask(IBrush mask, Rect bounds)
|
|
|
+ {
|
|
|
+ // TODO: This should be disposed
|
|
|
+ var paint = new SKPaint();
|
|
|
+
|
|
|
+ Canvas.SaveLayer(paint);
|
|
|
+ _maskStack.Push(CreatePaint(mask, bounds.Size));
|
|
|
+ }
|
|
|
|
|
|
- var solid = brush as ISolidColorBrush;
|
|
|
- if (solid != null)
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void PopOpacityMask()
|
|
|
+ {
|
|
|
+ using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn })
|
|
|
{
|
|
|
- paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
|
|
|
- return rv;
|
|
|
+ Canvas.SaveLayer(paint);
|
|
|
+ using (var paintWrapper = _maskStack.Pop())
|
|
|
+ {
|
|
|
+ Canvas.DrawPaint(paintWrapper.Paint);
|
|
|
+ }
|
|
|
+ Canvas.Restore();
|
|
|
}
|
|
|
- paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity)));
|
|
|
|
|
|
- var gradient = brush as IGradientBrush;
|
|
|
- if (gradient != null)
|
|
|
+ Canvas.Restore();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <inheritdoc />
|
|
|
+ public Matrix Transform
|
|
|
+ {
|
|
|
+ get { return _currentTransform; }
|
|
|
+ set
|
|
|
{
|
|
|
- var tileMode = gradient.SpreadMethod.ToSKShaderTileMode();
|
|
|
- var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
|
|
|
- var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray();
|
|
|
+ if (_currentTransform == value)
|
|
|
+ return;
|
|
|
+
|
|
|
+ _currentTransform = value;
|
|
|
+
|
|
|
+ var transform = value;
|
|
|
|
|
|
- var linearGradient = brush as ILinearGradientBrush;
|
|
|
- if (linearGradient != null)
|
|
|
+ if (_postTransform.HasValue)
|
|
|
+ {
|
|
|
+ transform *= _postTransform.Value;
|
|
|
+ }
|
|
|
+
|
|
|
+ Canvas.SetMatrix(transform.ToSKMatrix());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Configure paint wrapper for using gradient brush.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="paintWrapper">Paint wrapper.</param>
|
|
|
+ /// <param name="targetSize">Target size.</param>
|
|
|
+ /// <param name="gradientBrush">Gradient brush.</param>
|
|
|
+ private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
|
|
|
+ {
|
|
|
+ var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
|
|
|
+ var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
|
|
|
+ var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
|
|
|
+
|
|
|
+ switch (gradientBrush)
|
|
|
+ {
|
|
|
+ case ILinearGradientBrush linearGradient:
|
|
|
{
|
|
|
var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
|
|
|
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
|
|
|
|
|
|
// would be nice to cache these shaders possibly?
|
|
|
- using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
|
|
|
- paint.Shader = shader;
|
|
|
+ using (var shader =
|
|
|
+ SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
|
|
|
+ {
|
|
|
+ paintWrapper.Paint.Shader = shader;
|
|
|
+ }
|
|
|
|
|
|
+ break;
|
|
|
}
|
|
|
- else
|
|
|
+ case IRadialGradientBrush radialGradient:
|
|
|
{
|
|
|
- var radialGradient = brush as IRadialGradientBrush;
|
|
|
- if (radialGradient != null)
|
|
|
- {
|
|
|
- var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
|
|
|
- var radius = (float)radialGradient.Radius;
|
|
|
+ var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
|
|
|
+ var radius = (float)(radialGradient.Radius * targetSize.Width);
|
|
|
|
|
|
- // TODO: There is no SetAlpha in SkiaSharp
|
|
|
- //paint.setAlpha(128);
|
|
|
-
|
|
|
- // would be nice to cache these shaders possibly?
|
|
|
- using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
|
|
|
- paint.Shader = shader;
|
|
|
+ // TODO: There is no SetAlpha in SkiaSharp
|
|
|
+ //paint.setAlpha(128);
|
|
|
|
|
|
+ // would be nice to cache these shaders possibly?
|
|
|
+ using (var shader =
|
|
|
+ SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
|
|
|
+ {
|
|
|
+ paintWrapper.Paint.Shader = shader;
|
|
|
}
|
|
|
+
|
|
|
+ break;
|
|
|
}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Configure paint wrapper for using tile brush.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="paintWrapper">Paint wrapper.</param>
|
|
|
+ /// <param name="targetSize">Target size.</param>
|
|
|
+ /// <param name="tileBrush">Tile brush to use.</param>
|
|
|
+ /// <param name="tileBrushImage">Tile brush image.</param>
|
|
|
+ private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
|
|
|
+ {
|
|
|
+ var calc = new TileBrushCalculator(tileBrush,
|
|
|
+ new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
|
|
|
+
|
|
|
+ var intermediate = CreateRenderTarget(
|
|
|
+ (int)calc.IntermediateSize.Width,
|
|
|
+ (int)calc.IntermediateSize.Height, _dpi);
|
|
|
+
|
|
|
+ paintWrapper.AddDisposable(intermediate);
|
|
|
+
|
|
|
+ using (var context = intermediate.CreateDrawingContext(null))
|
|
|
+ {
|
|
|
+ var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
|
|
|
|
|
|
- return rv;
|
|
|
+ context.Clear(Colors.Transparent);
|
|
|
+ context.PushClip(calc.IntermediateClip);
|
|
|
+ context.Transform = calc.IntermediateTransform;
|
|
|
+ context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
|
|
|
+ context.PopClip();
|
|
|
}
|
|
|
|
|
|
- var tileBrush = brush as ITileBrush;
|
|
|
- var visualBrush = brush as IVisualBrush;
|
|
|
- var tileBrushImage = default(BitmapImpl);
|
|
|
+ var tileTransform =
|
|
|
+ tileBrush.TileMode != TileMode.None
|
|
|
+ ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
|
|
|
+ : SKMatrix.MakeIdentity();
|
|
|
|
|
|
- if (visualBrush != null)
|
|
|
+ SKShaderTileMode tileX =
|
|
|
+ tileBrush.TileMode == TileMode.None
|
|
|
+ ? SKShaderTileMode.Clamp
|
|
|
+ : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
|
|
|
+ ? SKShaderTileMode.Mirror
|
|
|
+ : SKShaderTileMode.Repeat;
|
|
|
+
|
|
|
+ SKShaderTileMode tileY =
|
|
|
+ tileBrush.TileMode == TileMode.None
|
|
|
+ ? SKShaderTileMode.Clamp
|
|
|
+ : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
|
|
|
+ ? SKShaderTileMode.Mirror
|
|
|
+ : SKShaderTileMode.Repeat;
|
|
|
+
|
|
|
+
|
|
|
+ var image = intermediate.SnapshotImage();
|
|
|
+ paintWrapper.AddDisposable(image);
|
|
|
+
|
|
|
+ using (var shader = image.ToShader(tileX, tileY, tileTransform))
|
|
|
{
|
|
|
- if (_visualBrushRenderer != null)
|
|
|
- {
|
|
|
- var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
|
|
|
+ paintWrapper.Paint.Shader = shader;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
|
|
|
- {
|
|
|
- var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
|
|
|
+ /// <summary>
|
|
|
+ /// Configure paint wrapper to use visual brush.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="paintWrapper">Paint wrapper.</param>
|
|
|
+ /// <param name="visualBrush">Visual brush.</param>
|
|
|
+ /// <param name="visualBrushRenderer">Visual brush renderer.</param>
|
|
|
+ /// <param name="tileBrushImage">Tile brush image.</param>
|
|
|
+ private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage)
|
|
|
+ {
|
|
|
+ if (_visualBrushRenderer == null)
|
|
|
+ {
|
|
|
+ throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
|
|
|
+ }
|
|
|
|
|
|
- using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer))
|
|
|
- {
|
|
|
- ctx.Clear(Colors.Transparent);
|
|
|
- _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
|
|
|
- }
|
|
|
+ var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush);
|
|
|
|
|
|
- tileBrushImage = intermediate;
|
|
|
- rv.AddDisposable(tileBrushImage);
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
+ if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
|
|
|
+ {
|
|
|
+ var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
|
|
|
+
|
|
|
+ using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer))
|
|
|
{
|
|
|
- throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
|
|
|
+ ctx.Clear(Colors.Transparent);
|
|
|
+
|
|
|
+ visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
|
|
|
}
|
|
|
+
|
|
|
+ tileBrushImage = intermediate;
|
|
|
+ paintWrapper.AddDisposable(tileBrushImage);
|
|
|
}
|
|
|
- else
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Creates paint wrapper for given brush.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="brush">Source brush.</param>
|
|
|
+ /// <param name="targetSize">Target size.</param>
|
|
|
+ /// <returns>Paint wrapper for given brush.</returns>
|
|
|
+ internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
|
|
|
+ {
|
|
|
+ var paint = new SKPaint
|
|
|
+ {
|
|
|
+ IsStroke = false,
|
|
|
+ IsAntialias = true
|
|
|
+ };
|
|
|
+
|
|
|
+ var paintWrapper = new PaintWrapper(paint);
|
|
|
+
|
|
|
+ double opacity = brush.Opacity * _currentOpacity;
|
|
|
+
|
|
|
+ if (brush is ISolidColorBrush solid)
|
|
|
{
|
|
|
- tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item);
|
|
|
+ paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
|
|
|
+
|
|
|
+ return paintWrapper;
|
|
|
}
|
|
|
|
|
|
- if (tileBrush != null && tileBrushImage != null)
|
|
|
+ paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity));
|
|
|
+
|
|
|
+ if (brush is IGradientBrush gradient)
|
|
|
{
|
|
|
- var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
|
|
|
- var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi);
|
|
|
- rv.AddDisposable(bitmap);
|
|
|
- using (var context = bitmap.CreateDrawingContext(null))
|
|
|
- {
|
|
|
- var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
|
|
|
+ ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
|
|
|
|
|
|
- context.Clear(Colors.Transparent);
|
|
|
- context.PushClip(calc.IntermediateClip);
|
|
|
- context.Transform = calc.IntermediateTransform;
|
|
|
- context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
|
|
|
- context.PopClip();
|
|
|
- }
|
|
|
+ return paintWrapper;
|
|
|
+ }
|
|
|
+
|
|
|
+ var tileBrush = brush as ITileBrush;
|
|
|
+ var visualBrush = brush as IVisualBrush;
|
|
|
+ var tileBrushImage = default(IDrawableBitmapImpl);
|
|
|
|
|
|
- SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y);
|
|
|
- SKShaderTileMode tileX =
|
|
|
- tileBrush.TileMode == TileMode.None
|
|
|
- ? SKShaderTileMode.Clamp
|
|
|
- : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
|
|
|
- ? SKShaderTileMode.Mirror
|
|
|
- : SKShaderTileMode.Repeat;
|
|
|
-
|
|
|
- SKShaderTileMode tileY =
|
|
|
- tileBrush.TileMode == TileMode.None
|
|
|
- ? SKShaderTileMode.Clamp
|
|
|
- : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
|
|
|
- ? SKShaderTileMode.Mirror
|
|
|
- : SKShaderTileMode.Repeat;
|
|
|
- using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation))
|
|
|
- paint.Shader = shader;
|
|
|
+ if (visualBrush != null)
|
|
|
+ {
|
|
|
+ ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item;
|
|
|
}
|
|
|
|
|
|
- return rv;
|
|
|
+ if (tileBrush != null && tileBrushImage != null)
|
|
|
+ {
|
|
|
+ ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
|
|
|
+ }
|
|
|
+
|
|
|
+ return paintWrapper;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Creates paint wrapper for given pen.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="pen">Source pen.</param>
|
|
|
+ /// <param name="targetSize">Target size.</param>
|
|
|
+ /// <returns></returns>
|
|
|
private PaintWrapper CreatePaint(Pen pen, Size targetSize)
|
|
|
{
|
|
|
var rv = CreatePaint(pen.Brush, targetSize);
|
|
|
var paint = rv.Paint;
|
|
|
|
|
|
paint.IsStroke = true;
|
|
|
- paint.StrokeWidth = (float)pen.Thickness;
|
|
|
+ paint.StrokeWidth = (float) pen.Thickness;
|
|
|
|
|
|
- if (pen.StartLineCap == PenLineCap.Round)
|
|
|
- paint.StrokeCap = SKStrokeCap.Round;
|
|
|
- else if (pen.StartLineCap == PenLineCap.Square)
|
|
|
- paint.StrokeCap = SKStrokeCap.Square;
|
|
|
- else
|
|
|
- paint.StrokeCap = SKStrokeCap.Butt;
|
|
|
+ // Need to modify dashes due to Skia modifying their lengths
|
|
|
+ // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
|
|
|
+ // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
|
|
|
+ float dashLengthModifier;
|
|
|
+ float gapLengthModifier;
|
|
|
|
|
|
- if (pen.LineJoin == PenLineJoin.Miter)
|
|
|
- paint.StrokeJoin = SKStrokeJoin.Miter;
|
|
|
- else if (pen.LineJoin == PenLineJoin.Round)
|
|
|
- paint.StrokeJoin = SKStrokeJoin.Round;
|
|
|
- else
|
|
|
- paint.StrokeJoin = SKStrokeJoin.Bevel;
|
|
|
+ switch (pen.StartLineCap)
|
|
|
+ {
|
|
|
+ case PenLineCap.Round:
|
|
|
+ paint.StrokeCap = SKStrokeCap.Round;
|
|
|
+ dashLengthModifier = -paint.StrokeWidth;
|
|
|
+ gapLengthModifier = paint.StrokeWidth;
|
|
|
+ break;
|
|
|
+ case PenLineCap.Square:
|
|
|
+ paint.StrokeCap = SKStrokeCap.Square;
|
|
|
+ dashLengthModifier = -paint.StrokeWidth;
|
|
|
+ gapLengthModifier = paint.StrokeWidth;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ paint.StrokeCap = SKStrokeCap.Butt;
|
|
|
+ dashLengthModifier = 0.0f;
|
|
|
+ gapLengthModifier = 0.0f;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (pen.LineJoin)
|
|
|
+ {
|
|
|
+ case PenLineJoin.Miter:
|
|
|
+ paint.StrokeJoin = SKStrokeJoin.Miter;
|
|
|
+ break;
|
|
|
+ case PenLineJoin.Round:
|
|
|
+ paint.StrokeJoin = SKStrokeJoin.Round;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ paint.StrokeJoin = SKStrokeJoin.Bevel;
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- paint.StrokeMiter = (float)pen.MiterLimit;
|
|
|
+ paint.StrokeMiter = (float) pen.MiterLimit;
|
|
|
|
|
|
if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
|
|
|
{
|
|
|
- var pe = SKPathEffect.CreateDash(
|
|
|
- pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(),
|
|
|
- (float)pen.DashStyle.Offset);
|
|
|
+ var srcDashes = pen.DashStyle.Dashes;
|
|
|
+ var dashesArray = new float[srcDashes.Count];
|
|
|
+
|
|
|
+ for (var i = 0; i < srcDashes.Count; ++i)
|
|
|
+ {
|
|
|
+ var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier;
|
|
|
+
|
|
|
+ // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale
|
|
|
+ dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier;
|
|
|
+ }
|
|
|
+
|
|
|
+ var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset);
|
|
|
+
|
|
|
paint.PathEffect = pe;
|
|
|
rv.AddDisposable(pe);
|
|
|
}
|
|
@@ -314,128 +571,118 @@ namespace Avalonia.Skia
|
|
|
return rv;
|
|
|
}
|
|
|
|
|
|
- public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
|
|
|
+ /// <summary>
|
|
|
+ /// Create new render target compatible with this drawing context.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="width">Width.</param>
|
|
|
+ /// <param name="height">Height.</param>
|
|
|
+ /// <param name="dpi">Drawing dpi.</param>
|
|
|
+ /// <param name="format">Pixel format.</param>
|
|
|
+ /// <returns></returns>
|
|
|
+ private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null)
|
|
|
{
|
|
|
- using (var paint = CreatePaint(pen, rect.Size))
|
|
|
+ var createInfo = new SurfaceRenderTarget.CreateInfo
|
|
|
{
|
|
|
- var rc = rect.ToSKRect();
|
|
|
- if (cornerRadius == 0)
|
|
|
- {
|
|
|
- Canvas.DrawRect(rc, paint.Paint);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
|
|
|
- }
|
|
|
- }
|
|
|
+ Width = width,
|
|
|
+ Height = height,
|
|
|
+ Dpi = dpi,
|
|
|
+ Format = format,
|
|
|
+ DisableTextLcdRendering = !_canTextUseLcdRendering
|
|
|
+ };
|
|
|
+
|
|
|
+ return new SurfaceRenderTarget(createInfo);
|
|
|
}
|
|
|
|
|
|
- public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
|
|
|
+ /// <summary>
|
|
|
+ /// Skia cached paint state.
|
|
|
+ /// </summary>
|
|
|
+ private struct PaintState : IDisposable
|
|
|
{
|
|
|
- using (var paint = CreatePaint(brush, rect.Size))
|
|
|
+ private readonly SKColor _color;
|
|
|
+ private readonly SKShader _shader;
|
|
|
+ private readonly SKPaint _paint;
|
|
|
+
|
|
|
+ public PaintState(SKPaint paint, SKColor color, SKShader shader)
|
|
|
{
|
|
|
- var rc = rect.ToSKRect();
|
|
|
- if (cornerRadius == 0)
|
|
|
- {
|
|
|
- Canvas.DrawRect(rc, paint.Paint);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
|
|
|
- }
|
|
|
+ _paint = paint;
|
|
|
+ _color = color;
|
|
|
+ _shader = shader;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
|
|
|
- {
|
|
|
- using (var paint = CreatePaint(foreground, text.Size))
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void Dispose()
|
|
|
{
|
|
|
- var textImpl = (FormattedTextImpl)text;
|
|
|
- textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering);
|
|
|
+ _paint.Color = _color;
|
|
|
+ _paint.Shader = _shader;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public IRenderTargetBitmapImpl CreateLayer(Size size)
|
|
|
- {
|
|
|
- var pixelSize = size * (_dpi / 96);
|
|
|
- return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi);
|
|
|
- }
|
|
|
-
|
|
|
- public void PushClip(Rect clip)
|
|
|
- {
|
|
|
- Canvas.Save();
|
|
|
- Canvas.ClipRect(clip.ToSKRect());
|
|
|
- }
|
|
|
-
|
|
|
- public void PopClip()
|
|
|
- {
|
|
|
- Canvas.Restore();
|
|
|
- }
|
|
|
-
|
|
|
- private double _currentOpacity = 1.0f;
|
|
|
- private readonly Stack<double> _opacityStack = new Stack<double>();
|
|
|
-
|
|
|
- public void PushOpacity(double opacity)
|
|
|
+ /// <summary>
|
|
|
+ /// Skia paint wrapper.
|
|
|
+ /// </summary>
|
|
|
+ internal struct PaintWrapper : IDisposable
|
|
|
{
|
|
|
- _opacityStack.Push(_currentOpacity);
|
|
|
- _currentOpacity *= opacity;
|
|
|
- }
|
|
|
+ //We are saving memory allocations there
|
|
|
+ public readonly SKPaint Paint;
|
|
|
|
|
|
- public void PopOpacity()
|
|
|
- {
|
|
|
- _currentOpacity = _opacityStack.Pop();
|
|
|
- }
|
|
|
+ private IDisposable _disposable1;
|
|
|
+ private IDisposable _disposable2;
|
|
|
+ private IDisposable _disposable3;
|
|
|
|
|
|
- public virtual void Dispose()
|
|
|
- {
|
|
|
- if(_disposables!=null)
|
|
|
- foreach (var disposable in _disposables)
|
|
|
- disposable?.Dispose();
|
|
|
- }
|
|
|
+ public PaintWrapper(SKPaint paint)
|
|
|
+ {
|
|
|
+ Paint = paint;
|
|
|
|
|
|
- public void PushGeometryClip(IGeometryImpl clip)
|
|
|
- {
|
|
|
- Canvas.Save();
|
|
|
- Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath);
|
|
|
- }
|
|
|
+ _disposable1 = null;
|
|
|
+ _disposable2 = null;
|
|
|
+ _disposable3 = null;
|
|
|
+ }
|
|
|
|
|
|
- public void PopGeometryClip()
|
|
|
- {
|
|
|
- Canvas.Restore();
|
|
|
- }
|
|
|
+ public IDisposable ApplyTo(SKPaint paint)
|
|
|
+ {
|
|
|
+ var state = new PaintState(paint, paint.Color, paint.Shader);
|
|
|
|
|
|
- public void PushOpacityMask(IBrush mask, Rect bounds)
|
|
|
- {
|
|
|
- Canvas.SaveLayer(new SKPaint());
|
|
|
- maskStack.Push(CreatePaint(mask, bounds.Size));
|
|
|
- }
|
|
|
+ paint.Color = Paint.Color;
|
|
|
+ paint.Shader = Paint.Shader;
|
|
|
|
|
|
- public void PopOpacityMask()
|
|
|
- {
|
|
|
- Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn });
|
|
|
- using (var paintWrapper = maskStack.Pop())
|
|
|
- {
|
|
|
- Canvas.DrawPaint(paintWrapper.Paint);
|
|
|
+ return state;
|
|
|
}
|
|
|
- Canvas.Restore();
|
|
|
- Canvas.Restore();
|
|
|
- }
|
|
|
|
|
|
- private Matrix _currentTransform;
|
|
|
-
|
|
|
- public Matrix Transform
|
|
|
- {
|
|
|
- get { return _currentTransform; }
|
|
|
- set
|
|
|
+ /// <summary>
|
|
|
+ /// Add new disposable to a wrapper.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="disposable">Disposable to add.</param>
|
|
|
+ public void AddDisposable(IDisposable disposable)
|
|
|
{
|
|
|
- if (_currentTransform == value)
|
|
|
- return;
|
|
|
+ if (_disposable1 == null)
|
|
|
+ {
|
|
|
+ _disposable1 = disposable;
|
|
|
+ }
|
|
|
+ else if (_disposable2 == null)
|
|
|
+ {
|
|
|
+ _disposable2 = disposable;
|
|
|
+ }
|
|
|
+ else if (_disposable3 == null)
|
|
|
+ {
|
|
|
+ _disposable3 = disposable;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Debug.Assert(false);
|
|
|
|
|
|
- _currentTransform = value;
|
|
|
- var transform = value;
|
|
|
- if (_postTransform.HasValue)
|
|
|
- transform *= _postTransform.Value;
|
|
|
- Canvas.SetMatrix(transform.ToSKMatrix());
|
|
|
+ // ReSharper disable once HeuristicUnreachableCode
|
|
|
+ throw new InvalidOperationException(
|
|
|
+ "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <inheritdoc />
|
|
|
+ public void Dispose()
|
|
|
+ {
|
|
|
+ Paint?.Dispose();
|
|
|
+ _disposable1?.Dispose();
|
|
|
+ _disposable2?.Dispose();
|
|
|
+ _disposable3?.Dispose();
|
|
|
}
|
|
|
}
|
|
|
}
|