Browse Source

Fixes relative brush mapping mode (#14598)

* Fixed relative mapping mode for brushes (skia only, pending radial)

* Implemented separate RadiusX/RadiusY with proper mapping modes for RadialGradientBrush

* tests for conic brush

* Added tests for geometry drawing

* Updated DrawingBrush test image since it now matches WPF

* Update obsolete property usage

* Fixed D2D, updated radial test with D2D results that actually match WPF ones

* Fixed RadiusX/Y for radial gradients with non-centered origin

* Updated obsolete property usage

* Code cleanup

* Typo

* ApiDiff suppression

* Removed files for skipped test

* More info in obsoletion warning

* clarify

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/14564/head
Nikita Tsukanov 2 years ago
committed by GitHub
parent
commit
8329efddd3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Avalonia.Desktop.slnf
  2. 12
      api/Avalonia.nupkg.xml
  3. 10
      samples/RenderDemo/Pages/AnimationsPage.xaml
  4. 2
      samples/RenderDemo/Pages/BrushesPage.axaml
  5. 4
      samples/RenderDemo/Pages/TransitionsPage.xaml
  6. 1
      src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs
  7. 5
      src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs
  8. 20
      src/Avalonia.Base/Animation/Animators/RelativeScalarAnimator.cs
  9. 14
      src/Avalonia.Base/Media/IRadialGradientBrush.cs
  10. 37
      src/Avalonia.Base/Media/Immutable/ImmutableRadialGradientBrush.cs
  11. 52
      src/Avalonia.Base/Media/RadialGradientBrush.cs
  12. 16
      src/Avalonia.Base/RelativePoint.cs
  13. 16
      src/Avalonia.Base/RelativeRect.cs
  14. 113
      src/Avalonia.Base/RelativeScalar.cs
  15. 2
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs
  16. 3
      src/Avalonia.Base/composition-schema.xml
  17. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  18. 182
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  19. 2
      src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
  20. 34
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  21. 17
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  22. 6
      src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs
  23. 11
      src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
  24. 32
      tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
  25. 22
      tests/Avalonia.RenderTests/Media/ImageBrushTests.cs
  26. 24
      tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
  27. 71
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  28. 51
      tests/Avalonia.RenderTests/Media/RelativePointTestPrimitivesHelper.cs
  29. 23
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  30. BIN
      tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Absolute.expected.png
  31. BIN
      tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Relative.expected.png
  32. BIN
      tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png
  33. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Absolute.expected.png
  34. BIN
      tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Relative.expected.png
  35. BIN
      tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Absolute.expected.png
  36. BIN
      tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Relative.expected.png
  37. BIN
      tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_CenterOrigin.expected.png
  38. BIN
      tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_MovedOrigin.expected.png
  39. BIN
      tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_CenterOrigin.expected.png
  40. BIN
      tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_MovedOrigin.expected.png
  41. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Absolute.expected.png
  42. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Relative.expected.png
  43. BIN
      tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Absolute.expected.png
  44. BIN
      tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Relative.expected.png
  45. BIN
      tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png
  46. BIN
      tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png
  47. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Absolute.expected.png
  48. BIN
      tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Relative.expected.png
  49. BIN
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Absolute.expected.png
  50. BIN
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Relative.expected.png
  51. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_CenterOrigin.expected.png
  52. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_MovedOrigin.expected.png
  53. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_CenterOrigin.expected.png
  54. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_MovedOrigin.expected.png
  55. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_With_Different_Radius_Is_Properly_Rotated_CenterOrigin.expected.png
  56. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_With_Different_Radius_Is_Properly_Rotated_MovedOrigin.expected.png
  57. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Absolute.expected.png
  58. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Relative.expected.png

1
Avalonia.Desktop.slnf

@ -11,6 +11,7 @@
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\RenderDemo\\RenderDemo.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",

12
api/Avalonia.nupkg.xml

@ -1075,4 +1075,16 @@
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IRadialGradientBrush.RadiusX</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Media.IRadialGradientBrush.RadiusY</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>

10
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -267,7 +267,7 @@
PlaybackDirection="Normal">
<KeyFrame Cue="0%">
<Setter Property="Background">
<RadialGradientBrush Center="0%,100%" Radius="0.8">
<RadialGradientBrush Center="0%,100%" RadiusX="80%" RadiusY="80%">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</RadialGradientBrush>
@ -275,7 +275,7 @@
</KeyFrame>
<KeyFrame Cue="25%">
<Setter Property="Background">
<RadialGradientBrush Center="0%,0%" Radius="1">
<RadialGradientBrush Center="0%,0%" RadiusX="100%" RadiusY="100%">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</RadialGradientBrush>
@ -283,7 +283,7 @@
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="Background">
<RadialGradientBrush Center="100%,0%" Radius="0.8">
<RadialGradientBrush Center="100%,0%" RadiusX="80%" RadiusY="80%">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</RadialGradientBrush>
@ -291,7 +291,7 @@
</KeyFrame>
<KeyFrame Cue="75%">
<Setter Property="Background">
<RadialGradientBrush Center="100%,100%" Radius="1">
<RadialGradientBrush Center="100%,100%" RadiusX="100%" RadiusY="100%">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</RadialGradientBrush>
@ -299,7 +299,7 @@
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Background">
<RadialGradientBrush Center="0%,100%" Radius="0.8">
<RadialGradientBrush Center="0%,100%" RadiusX="80%" RadiusY="80%">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</RadialGradientBrush>

2
samples/RenderDemo/Pages/BrushesPage.axaml

@ -27,7 +27,7 @@
<TextBlock Canvas.Left="20" Canvas.Top="70" FontSize="30" Text="scale(0.5) on gradient" />
<Rectangle Canvas.Left="20" Canvas.Top="110" Width="440" Height="50">
<Rectangle.Fill>
<RadialGradientBrush Center="0.0,0.0" GradientOrigin="0.0,0.0" Radius="0.13636364">
<RadialGradientBrush Center="0.0,0.0" GradientOrigin="0.0,0.0" RadiusX="13%" RadiusY="13%">
<RadialGradientBrush.Transform>
<TransformGroup>
<ScaleTransform />

4
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -228,7 +228,7 @@
</Transitions>
</Setter>
<Setter Property="Background" >
<RadialGradientBrush Center="50%,50%" Radius="0.5">
<RadialGradientBrush Center="50%,50%" RadiusX="50%" RadiusY="50%">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</RadialGradientBrush>
@ -237,7 +237,7 @@
<Style Selector="Border.Rect14:pointerover">
<Setter Property="Background" >
<RadialGradientBrush Center="30%,30%" Radius="0.2">
<RadialGradientBrush Center="30%,30%" RadiusX="20%" RadiusY="20%">
<GradientStop Offset="0" Color="Green"/>
<GradientStop Offset="1" Color="Yellow"/>
</RadialGradientBrush>

1
src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs

@ -58,6 +58,7 @@ partial class Animation
RegisterAnimator<Point, PointAnimator>();
RegisterAnimator<Rect, RectAnimator>();
RegisterAnimator<RelativePoint, RelativePointAnimator>();
RegisterAnimator<RelativeScalar, RelativeScalarAnimator>();
RegisterAnimator<Size, SizeAnimator>();
RegisterAnimator<Thickness, ThicknessAnimator>();
}

5
src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs

@ -16,6 +16,7 @@ namespace Avalonia.Animation.Animators
internal class GradientBrushAnimator : Animator<IGradientBrush?>
{
private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator();
private static readonly RelativeScalarAnimator s_relativeScalarAnimator = new RelativeScalarAnimator();
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
public override IGradientBrush? Interpolate(double progress, IGradientBrush? oldValue, IGradientBrush? newValue)
@ -36,7 +37,9 @@ namespace Avalonia.Animation.Animators
oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center),
s_relativePointAnimator.Interpolate(progress, oldRadial.GradientOrigin, newRadial.GradientOrigin),
s_doubleAnimator.Interpolate(progress, oldRadial.Radius, newRadial.Radius));
s_relativeScalarAnimator.Interpolate(progress, oldRadial.RadiusX, newRadial.RadiusX),
s_relativeScalarAnimator.Interpolate(progress, oldRadial.RadiusY, newRadial.RadiusY)
);
case IConicGradientBrush oldConic when newValue is IConicGradientBrush newConic:
return new ImmutableConicGradientBrush(

20
src/Avalonia.Base/Animation/Animators/RelativeScalarAnimator.cs

@ -0,0 +1,20 @@
namespace Avalonia.Animation.Animators
{
/// <summary>
/// Animator that handles <see cref="RelativeScalar"/> properties.
/// </summary>
internal class RelativeScalarAnimator : Animator<RelativeScalar>
{
private static readonly DoubleAnimator s_scalarAnimator = new DoubleAnimator();
public override RelativeScalar Interpolate(double progress, RelativeScalar oldValue, RelativeScalar newValue)
{
if (oldValue.Unit != newValue.Unit)
{
return progress >= 0.5 ? newValue : oldValue;
}
return new RelativeScalar(s_scalarAnimator.Interpolate(progress, oldValue.Scalar, newValue.Scalar), oldValue.Unit);
}
}
}

14
src/Avalonia.Base/Media/IRadialGradientBrush.cs

@ -1,4 +1,5 @@
using Avalonia.Metadata;
using System;
using Avalonia.Metadata;
namespace Avalonia.Media
{
@ -19,9 +20,16 @@ namespace Avalonia.Media
/// </summary>
RelativePoint GradientOrigin { get; }
[Obsolete("Use RadiusX/RadiusY")] public double Radius { get; }
/// <summary>
/// Gets the horizontal radius of the outermost circle of the radial gradient.
/// </summary>
RelativeScalar RadiusX { get; }
/// <summary>
/// Gets the horizontal and vertical radius of the outermost circle of the radial gradient.
/// Gets the vertical radius of the outermost circle of the radial gradient.
/// </summary>
double Radius { get; }
RelativeScalar RadiusY { get; }
}
}

37
src/Avalonia.Base/Media/Immutable/ImmutableRadialGradientBrush.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace Avalonia.Media.Immutable
{
@ -31,13 +32,35 @@ namespace Avalonia.Media.Immutable
RelativePoint? center = null,
RelativePoint? gradientOrigin = null,
double radius = 0.5)
: this(gradientStops, opacity, transform, transformOrigin, spreadMethod,
center, gradientOrigin,
new RelativeScalar(radius, RelativeUnit.Relative),
new RelativeScalar(radius, RelativeUnit.Relative)
)
{
}
public ImmutableRadialGradientBrush(
IReadOnlyList<ImmutableGradientStop> gradientStops,
double opacity = 1,
ImmutableTransform? transform = null,
RelativePoint? transformOrigin = null,
GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad,
RelativePoint? center = null,
RelativePoint? gradientOrigin = null,
RelativeScalar? radiusX = null,
RelativeScalar? radiusY = null
)
: base(gradientStops, opacity, transform, transformOrigin, spreadMethod)
{
Center = center ?? RelativePoint.Center;
GradientOrigin = gradientOrigin ?? RelativePoint.Center;
Radius = radius;
RadiusX = radiusX ?? RelativeScalar.Middle;
RadiusY = radiusY ?? RelativeScalar.Middle;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableRadialGradientBrush"/> class.
/// </summary>
@ -47,7 +70,8 @@ namespace Avalonia.Media.Immutable
{
Center = source.Center;
GradientOrigin = source.GradientOrigin;
Radius = source.Radius;
RadiusX = source.RadiusX;
RadiusY = source.RadiusX;
}
/// <inheritdoc/>
@ -57,6 +81,11 @@ namespace Avalonia.Media.Immutable
public RelativePoint GradientOrigin { get; }
/// <inheritdoc/>
public double Radius { get; }
public RelativeScalar RadiusX { get; }
/// <inheritdoc/>
public RelativeScalar RadiusY { get; }
[Obsolete("Use RadiusX/RadiusY")] public double Radius => RadiusX.Scalar;
}
}

52
src/Avalonia.Base/Media/RadialGradientBrush.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Media.Immutable;
using Avalonia.Metadata;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
@ -35,6 +36,20 @@ namespace Avalonia.Media
nameof(Radius),
0.5);
/// <summary>
/// Defines the <see cref="RadiusX"/> property.
/// </summary>
public static readonly StyledProperty<RelativeScalar> RadiusXProperty =
AvaloniaProperty.Register<RadialGradientBrush, RelativeScalar>(
nameof(RadiusX), RelativeScalar.Middle);
/// <summary>
/// Defines the <see cref="RadiusX"/> property.
/// </summary>
public static readonly StyledProperty<RelativeScalar> RadiusYProperty =
AvaloniaProperty.Register<RadialGradientBrush, RelativeScalar>(
nameof(RadiusY), RelativeScalar.Middle);
/// <summary>
/// Gets or sets the start point for the gradient.
/// </summary>
@ -54,11 +69,33 @@ namespace Avalonia.Media
set { SetValue(GradientOriginProperty, value); }
}
/// <summary>
/// Gets or sets the horizontal radius of the outermost circle of the radial
/// gradient.
/// </summary>
[DependsOn(nameof(Radius))]
public RelativeScalar RadiusX
{
get { return GetValue(RadiusXProperty); }
set { SetValue(RadiusXProperty, value); }
}
/// <summary>
/// Gets or sets the vertical radius of the outermost circle of the radial
/// gradient.
/// </summary>
[DependsOn(nameof(Radius))]
public RelativeScalar RadiusY
{
get { return GetValue(RadiusYProperty); }
set { SetValue(RadiusYProperty, value); }
}
/// <summary>
/// Gets or sets the horizontal and vertical radius of the outermost circle of the radial
/// gradient.
/// </summary>
// TODO: This appears to always be relative so should use a RelativeSize struct or something.
[Obsolete("Use RadiusX/RadiusY, note that those properties use _relative_ values, so Radius=0.55 would become RadiusX=55% RadiusY=55%. Radius property is always relative even if the rest of the brush uses absolute values.")]
public double Radius
{
get { return GetValue(RadiusProperty); }
@ -77,7 +114,18 @@ namespace Avalonia.Media
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
ServerCompositionSimpleRadialGradientBrush.SerializeAllChanges(writer, Center, GradientOrigin, Radius);
ServerCompositionSimpleRadialGradientBrush.SerializeAllChanges(writer, Center, GradientOrigin, RadiusX, RadiusY);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.IsEffectiveValueChange && change.Property == RadiusProperty)
{
var compatibilityValue = new RelativeScalar(Radius, RelativeUnit.Relative);
SetCurrentValue(RadiusXProperty, compatibilityValue);
SetCurrentValue(RadiusYProperty, compatibilityValue);
}
base.OnPropertyChanged(change);
}
}
}

16
src/Avalonia.Base/RelativePoint.cs

@ -142,13 +142,27 @@ namespace Avalonia
/// </summary>
/// <param name="size">The size of the visual.</param>
/// <returns>The origin point in pixels.</returns>
[Obsolete("Use ToPixels(Rect) overload to properly map relative points")]
public Point ToPixels(Size size)
{
return _unit == RelativeUnit.Absolute ?
_point :
new Point(_point.X * size.Width, _point.Y * size.Height);
}
#if !BUILDTASK
/// <summary>
/// Converts a <see cref="RelativePoint"/> into pixels.
/// </summary>
/// <param name="rect">The bounding box of the rendering primitive.</param>
/// <returns>The origin point in pixels.</returns>
public Point ToPixels(Rect rect)
{
return _unit == RelativeUnit.Absolute
? _point
: new Point(rect.X + _point.X * rect.Width, rect.Y + _point.Y * rect.Height);
}
#endif
/// <summary>
/// Parses a <see cref="RelativePoint"/> string.
/// </summary>

16
src/Avalonia.Base/RelativeRect.cs

@ -152,6 +152,22 @@ namespace Avalonia
Rect.Width * size.Width,
Rect.Height * size.Height);
}
/// <summary>
/// Converts a <see cref="RelativeRect"/> into pixels.
/// </summary>
/// <param name="boundingBox">The bounding box of the visual.</param>
/// <returns>The origin point in pixels.</returns>
public Rect ToPixels(Rect boundingBox)
{
return Unit == RelativeUnit.Absolute ?
Rect :
new Rect(
boundingBox.X + Rect.X * boundingBox.Width,
boundingBox.Y + Rect.Y * boundingBox.Height,
Rect.Width * boundingBox.Width,
Rect.Height * boundingBox.Height);
}
/// <summary>
/// Parses a <see cref="RelativeRect"/> string.

113
src/Avalonia.Base/RelativeScalar.cs

@ -0,0 +1,113 @@
using System;
using System.Globalization;
using Avalonia.Utilities;
namespace Avalonia;
/// <summary>
/// Defines a scalar value that may be defined relative to a containing element.
/// </summary>
public struct RelativeScalar : IEquatable<RelativeScalar>
{
private readonly double _scalar;
private readonly RelativeUnit _unit;
/// <summary>
/// Initializes a new instance of the <see cref="RelativeScalar"/> struct.
/// </summary>
/// <param name="scalar">The scalar value.</param>
/// <param name="unit">The unit.</param>
public RelativeScalar(double scalar, RelativeUnit unit)
{
_scalar = scalar;
_unit = unit;
}
/// <summary>
/// Gets the scalar.
/// </summary>
public double Scalar => _scalar;
/// <summary>
/// Gets the unit.
/// </summary>
public RelativeUnit Unit => _unit;
/// <summary>
/// The value at the beginning of the range
/// </summary>
public static RelativeScalar Beginning { get; } = new RelativeScalar(0, RelativeUnit.Relative);
/// <summary>
/// The value at the middle of the range
/// </summary>
public static RelativeScalar Middle { get; } = new RelativeScalar(0.5, RelativeUnit.Relative);
/// <summary>
/// The value at the end of the range
/// </summary>
public static RelativeScalar End { get; } = new RelativeScalar(1, RelativeUnit.Relative);
public bool Equals(RelativeScalar other)
{
return _scalar.Equals(other._scalar) && _unit == other._unit;
}
public override bool Equals(object? obj)
{
return obj is RelativeScalar other && Equals(other);
}
public override int GetHashCode()
{
return _scalar.GetHashCode() ^ (int)_unit;
}
public static bool operator ==(RelativeScalar left, RelativeScalar right)
{
return left.Equals(right);
}
public static bool operator !=(RelativeScalar left, RelativeScalar right)
{
return !left.Equals(right);
}
/// <summary>
/// Converts a <see cref="RelativeScalar"/> into a final value.
/// </summary>
/// <returns>The origin point in pixels.</returns>
public double ToValue(double size)
{
return _unit == RelativeUnit.Absolute
? _scalar
: size * _scalar;
}
/// <summary>
/// Parses a <see cref="RelativeScalar"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The parsed <see cref="RelativeScalar"/>.</returns>
public static RelativeScalar Parse(string s)
{
var trimmed = s.Trim();
if (trimmed.EndsWith("%"))
return new RelativeScalar(double.Parse(trimmed.TrimEnd('%'), CultureInfo.InvariantCulture) * 0.01,
RelativeUnit.Relative);
return new RelativeScalar(double.Parse(trimmed, CultureInfo.InvariantCulture), RelativeUnit.Absolute);
}
/// <summary>
/// Returns a String representing this RelativeScalar instance.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString()
{
return _unit == RelativeUnit.Absolute
? _scalar.ToString(CultureInfo.InvariantCulture)
: string.Format(CultureInfo.InvariantCulture, "{0}%", _scalar * 100);
}
}

2
src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs

@ -49,7 +49,7 @@ namespace Avalonia.Rendering.Composition.Server
partial class ServerCompositionSimpleRadialGradientBrush : IRadialGradientBrush
{
public double Radius => RadiusX.Scalar;
}
partial class ServerCompositionSimpleSolidColorBrush : ISolidColorBrush

3
src/Avalonia.Base/composition-schema.xml

@ -93,7 +93,8 @@
<Object Name="CompositionSimpleRadialGradientBrush" Internal="true" ServerOnly="true" ServerBase="ServerCompositionSimpleGradientBrush">
<Property Name="Center" Type="Avalonia.RelativePoint" />
<Property Name="GradientOrigin" Type="Avalonia.RelativePoint" />
<Property Name="Radius" Type="double" />
<Property Name="RadiusX" Type="Avalonia.RelativeScalar" />
<Property Name="RadiusY" Type="Avalonia.RelativeScalar" />
</Object>
<Object Name="CompositionSimpleLinearGradientBrush" Internal="true" ServerOnly="true" ServerBase="ServerCompositionSimpleGradientBrush">
<Property Name="StartPoint" Type="Avalonia.RelativePoint" />

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -31,7 +31,7 @@
<DrawingGroup>
<GeometryDrawing Geometry="F1 M 13.4533,17.56C 13.4533,19.8827 15.344,21.772 17.6653,21.772L 17.6653,21.772C 19.988,21.772 21.8773,19.8827 21.8773,17.56L 21.8773,17.56C 21.8773,15.2373 19.988,13.348 17.6653,13.348L 17.6653,13.348C 15.344,13.348 13.4533,15.2373 13.4533,17.56 Z ">
<GeometryDrawing.Brush>
<RadialGradientBrush Center="0.245696,0.288009" GradientOrigin="0.245696,0.288009" Radius="0.499952">
<RadialGradientBrush Center="0.245696,0.288009" GradientOrigin="0.245696,0.288009" RadiusX="50%" RadiusY="50%">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF878A8C" Offset="0" />
<GradientStop Color="#FF544A4C" Offset="0.991379" />
@ -41,7 +41,7 @@
</GeometryDrawing>
<GeometryDrawing Geometry="F1 M 13.332,6.22803L 10.2227,9.72668L 8.49866,9.72668L 8.49866,7.56136L 5.33333,7.56136L 5.33333,9.72668L 3.33333,9.72668L 3.33333,24.3947L 13.1213,24.3947C 14.424,25.264 15.9853,25.772 17.6653,25.772L 17.6653,25.772C 19.344,25.772 20.9067,25.264 22.2094,24.3947L 28.6667,24.3947L 28.6667,9.72668L 24.944,9.72668L 21.8333,6.22803M 12.12,17.56C 12.12,14.5013 14.608,12.0147 17.6653,12.0147L 17.6653,12.0147C 20.7227,12.0147 23.2107,14.5013 23.2107,17.56L 23.2107,17.56C 23.2107,20.6174 20.7227,23.104 17.6653,23.104L 17.6653,23.104C 14.608,23.104 12.12,20.6174 12.12,17.56 Z ">
<GeometryDrawing.Brush>
<RadialGradientBrush Center="0.196943,0.216757" GradientOrigin="0.196943,0.216757" Radius="0.44654">
<RadialGradientBrush Center="0.196943,0.216757" GradientOrigin="0.196943,0.216757" RadiusX="50%" RadiusY="50%">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF87898C" Offset="0" />
<GradientStop Color="#FF544A4C" Offset="1" />

182
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -215,7 +215,7 @@ namespace Avalonia.Skia
CheckLease();
if (pen is not null
&& TryCreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))) is { } stroke)
&& TryCreatePaint(_strokePaint, pen, new Rect(p1, p2).Normalize()) is { } stroke)
{
using (stroke)
{
@ -229,11 +229,11 @@ namespace Avalonia.Skia
{
CheckLease();
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
var rect = geometry.Bounds;
if (brush is not null && impl.FillPath != null)
{
using (var fill = CreatePaint(_fillPaint, brush, size))
using (var fill = CreatePaint(_fillPaint, brush, rect))
{
Canvas.DrawPath(impl.FillPath, fill.Paint);
}
@ -241,7 +241,7 @@ namespace Avalonia.Skia
if (pen is not null
&& impl.StrokePath != null
&& TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
&& TryCreatePaint(_strokePaint, pen, rect.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
@ -418,7 +418,7 @@ namespace Avalonia.Skia
if (brush != null)
{
using (var fill = CreatePaint(_fillPaint, brush, rect.Rect.Size))
using (var fill = CreatePaint(_fillPaint, brush, rect.Rect))
{
if (isRounded)
{
@ -461,7 +461,7 @@ namespace Avalonia.Skia
}
if (pen is not null
&& TryCreatePaint(_strokePaint, pen, rect.Rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
&& TryCreatePaint(_strokePaint, pen, rect.Rect.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
@ -491,14 +491,14 @@ namespace Avalonia.Skia
if (brush != null)
{
using (var fill = CreatePaint(_fillPaint, brush, rect.Size))
using (var fill = CreatePaint(_fillPaint, brush, rect))
{
Canvas.DrawOval(rc, fill.Paint);
}
}
if (pen is not null
&& TryCreatePaint(_strokePaint, pen, rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
&& TryCreatePaint(_strokePaint, pen, rect.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
@ -517,7 +517,7 @@ namespace Avalonia.Skia
return;
}
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun;
@ -706,7 +706,7 @@ namespace Avalonia.Skia
var paint = SKPaintCache.Shared.Get();
Canvas.SaveLayer(bounds.ToSKRect(), paint);
_maskStack.Push(CreatePaint(paint, mask, bounds.Size));
_maskStack.Push(CreatePaint(paint, mask, bounds));
}
/// <inheritdoc />
@ -767,9 +767,9 @@ namespace Avalonia.Skia
/// Configure paint wrapper for using gradient brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="targetRect">Target rect.</param>
/// <param name="gradientBrush">Gradient brush.</param>
private static void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
private static void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush)
{
var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
@ -779,8 +779,8 @@ namespace Avalonia.Skia
{
case ILinearGradientBrush linearGradient:
{
var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
var start = linearGradient.StartPoint.ToPixels(targetRect).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetRect).ToSKPoint();
// would be nice to cache these shaders possibly?
if (linearGradient.Transform is null)
@ -793,7 +793,7 @@ namespace Avalonia.Skia
}
else
{
var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetSize);
var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * linearGradient.Transform.Value * (offset);
@ -808,39 +808,55 @@ namespace Avalonia.Skia
}
case IRadialGradientBrush radialGradient:
{
var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
var radius = (float)(radialGradient.Radius * targetSize.Width);
var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint();
if (origin.Equals(center))
var centerPoint = radialGradient.Center.ToPixels(targetRect);
var center = centerPoint.ToSKPoint();
var radiusX = (radialGradient.RadiusX.ToValue(targetRect.Width));
var radiusY = (radialGradient.RadiusY.ToValue(targetRect.Height));
var originPoint = radialGradient.GradientOrigin.ToPixels(targetRect);
Matrix? transform = null;
if (radiusX != radiusY)
transform =
Matrix.CreateTranslation(-centerPoint)
* Matrix.CreateScale(1, radiusY / radiusX)
* Matrix.CreateTranslation(centerPoint);
if (radialGradient.Transform != null)
{
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var brushTransform = (-offset) * radialGradient.Transform.Value * (offset);
transform = transform.HasValue ? transform * brushTransform : brushTransform;
}
if (originPoint.Equals(centerPoint))
{
// when the origin is the same as the center the Skia RadialGradient acts the same as D2D
if (radialGradient.Transform is null)
{
using (var shader =
SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
{
paintWrapper.Paint.Shader = shader;
}
}
else
using (var shader =
transform.HasValue
? SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode,
transform.Value.ToSKMatrix())
: SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode)
)
{
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * radialGradient.Transform.Value * (offset);
using (var shader =
SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
{
paintWrapper.Paint.Shader = shader;
}
paintWrapper.Paint.Shader = shader;
}
}
else
{
// when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D
if (radiusX != radiusY)
// Adjust the origin point for radiusX/Y transformation by reversing it
originPoint = originPoint.WithY(
(originPoint.Y - centerPoint.Y) * radiusX / radiusY + centerPoint.Y);
var origin = originPoint.ToSKPoint();
// reverse the order of the stops to match D2D
var reversedColors = new SKColor[stopColors.Length];
Array.Copy(stopColors, reversedColors, stopColors.Length);
@ -858,30 +874,18 @@ namespace Avalonia.Skia
}
// compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
if (radialGradient.Transform is null)
using (var shader = SKShader.CreateCompose(
SKShader.CreateColor(reversedColors[0]),
transform.HasValue
? SKShader.CreateTwoPointConicalGradient(center, (float)radiusX, origin, 0,
reversedColors, reversedStops, tileMode, transform.Value.ToSKMatrix())
: SKShader.CreateTwoPointConicalGradient(center, (float)radiusX, origin, 0,
reversedColors, reversedStops, tileMode)
)
)
{
using (var shader = SKShader.CreateCompose(
SKShader.CreateColor(reversedColors[0]),
SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode)
))
{
paintWrapper.Paint.Shader = shader;
}
}
else
{
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * radialGradient.Transform.Value * (offset);
using (var shader = SKShader.CreateCompose(
SKShader.CreateColor(reversedColors[0]),
SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode, transform.ToSKMatrix())
))
{
paintWrapper.Paint.Shader = shader;
}
paintWrapper.Paint.Shader = shader;
}
}
@ -889,7 +893,7 @@ namespace Avalonia.Skia
}
case IConicGradientBrush conicGradient:
{
var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint();
var center = conicGradient.Center.ToPixels(targetRect).ToSKPoint();
// Skia's default is that angle 0 is from the right hand side of the center point
// but we are matching CSS where the vertical point above the center is 0.
@ -899,7 +903,7 @@ namespace Avalonia.Skia
if (conicGradient.Transform is { })
{
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetSize);
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * conicGradient.Transform.Value * (offset);
@ -921,12 +925,12 @@ namespace Avalonia.Skia
/// Configure paint wrapper for using tile brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="targetBox">Target bounding box.</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)
private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Rect targetBox, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
{
var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_dpi), targetSize);
var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_dpi), targetBox.Size);
var intermediate = CreateRenderTarget(calc.IntermediateSize, false);
paintWrapper.AddDisposable(intermediate);
@ -982,13 +986,17 @@ namespace Avalonia.Skia
if (tileBrush.Transform is { })
{
var origin = tileBrush.TransformOrigin.ToPixels(targetSize);
var origin = tileBrush.TransformOrigin.ToPixels(targetBox);
var offset = Matrix.CreateTranslation(origin);
var transform = (-offset) * tileBrush.Transform.Value * (offset);
paintTransform = paintTransform.PreConcat(transform.ToSKMatrix());
}
if (tileBrush.DestinationRect.Unit == RelativeUnit.Relative)
paintTransform =
paintTransform.PreConcat(SKMatrix.CreateTranslation((float)targetBox.X, (float)targetBox.Y));
using (var shader = image.ToShader(tileX, tileY, paintTransform))
{
paintWrapper.Paint.Shader = shader;
@ -996,16 +1004,16 @@ namespace Avalonia.Skia
}
private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
Rect targetRect)
{
if(content.UseScalableRasterization)
ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize);
ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetRect);
else
ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize);
ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetRect);
}
private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
Rect targetRect)
{
var rect = content.Rect;
var intermediateSize = rect.Size;
@ -1021,12 +1029,12 @@ namespace Avalonia.Skia
content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y));
}
ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate);
ConfigureTileBrush(ref paintWrapper, targetRect, content.Brush, intermediate);
}
}
private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
Rect targetRect)
{
var rect = content.Rect;
var contentSize = rect.Size;
@ -1039,7 +1047,7 @@ namespace Avalonia.Skia
var tileBrush = content.Brush;
var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y);
var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize);
var calc = new TileBrushCalculator(tileBrush, contentSize, targetRect.Size);
transform *= calc.IntermediateTransform;
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi);
@ -1077,13 +1085,17 @@ namespace Avalonia.Skia
if (tileBrush.Transform is { })
{
var origin = tileBrush.TransformOrigin.ToPixels(targetSize);
var origin = tileBrush.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(origin);
var brushTransform = (-offset) * tileBrush.Transform.Value * (offset);
paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix());
}
if (tileBrush.DestinationRect.Unit == RelativeUnit.Relative)
paintTransform =
paintTransform.PreConcat(SKMatrix.CreateTranslation((float)targetRect.X, (float)targetRect.Y));
using (var shader = picture.ToShader(tileX, tileY, paintTransform,
new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height)))
{
@ -1175,9 +1187,9 @@ namespace Avalonia.Skia
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="targetRect">Target rect.</param>
/// <returns>Paint wrapper for given brush.</returns>
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize)
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Rect targetRect)
{
var paintWrapper = new PaintWrapper(paint);
@ -1196,7 +1208,7 @@ namespace Avalonia.Skia
if (brush is IGradientBrush gradient)
{
ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
ConfigureGradientBrush(ref paintWrapper, targetRect, gradient);
return paintWrapper;
}
@ -1210,7 +1222,7 @@ namespace Avalonia.Skia
{
if (content != null)
{
ConfigureSceneBrushContent(ref paintWrapper, content, targetSize);
ConfigureSceneBrushContent(ref paintWrapper, content, targetRect);
return paintWrapper;
}
else
@ -1219,7 +1231,7 @@ namespace Avalonia.Skia
}
else if (brush is ISceneBrushContent sceneBrushContent)
{
ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize);
ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetRect);
return paintWrapper;
}
else
@ -1229,7 +1241,7 @@ namespace Avalonia.Skia
if (tileBrush != null && tileBrushImage != null)
{
ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
ConfigureTileBrush(ref paintWrapper, targetRect, tileBrush, tileBrushImage);
}
else
{
@ -1244,9 +1256,9 @@ namespace Avalonia.Skia
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="targetRect">Target rect.</param>
/// <returns></returns>
private PaintWrapper? TryCreatePaint(SKPaint paint, IPen pen, Size targetSize)
private PaintWrapper? TryCreatePaint(SKPaint paint, IPen pen, Rect targetRect)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
@ -1255,7 +1267,7 @@ namespace Avalonia.Skia
return null;
}
var rv = CreatePaint(paint, brush, targetSize);
var rv = CreatePaint(paint, brush, targetRect);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;

2
src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs

@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1.Media
// TODO: Work out how to get the size below rather than passing new Size().
var brush = (wrapper == null) ?
_foreground :
_context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush;
_context.CreateBrush(wrapper.Brush, default).PlatformBrush;
_renderTarget.DrawGlyphRun(
new RawVector2 { X = baselineOriginX, Y = baselineOriginY },

34
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -208,7 +208,7 @@ namespace Avalonia.Direct2D1.Media
using (var d2dSource = ((BitmapImpl)source).GetDirect2DBitmap(_deviceContext))
using (var sourceBrush = new BitmapBrush1(_deviceContext, d2dSource.Value, new BitmapBrushProperties1 { InterpolationMode = interpolationMode }))
using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size))
using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect))
using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D()))
{
if (d2dOpacityMask.PlatformBrush != null)
@ -233,9 +233,9 @@ namespace Avalonia.Direct2D1.Media
{
if (pen != null)
{
var size = new Rect(p1, p2).Size;
var bounds = new Rect(p1, p2);
using (var d2dBrush = CreateBrush(pen.Brush, size))
using (var d2dBrush = CreateBrush(pen.Brush, bounds))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@ -261,7 +261,7 @@ namespace Avalonia.Direct2D1.Media
{
if (brush != null)
{
using (var d2dBrush = CreateBrush(brush, geometry.Bounds.Size))
using (var d2dBrush = CreateBrush(brush, geometry.Bounds))
{
if (d2dBrush.PlatformBrush != null)
{
@ -273,7 +273,7 @@ namespace Avalonia.Direct2D1.Media
if (pen != null)
{
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size))
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen)))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@ -298,7 +298,7 @@ namespace Avalonia.Direct2D1.Media
if (brush != null)
{
using (var b = CreateBrush(brush, rect.Size))
using (var b = CreateBrush(brush, rect))
{
if (b.PlatformBrush != null)
{
@ -327,7 +327,7 @@ namespace Avalonia.Direct2D1.Media
if (pen?.Brush != null)
{
using (var wrapper = CreateBrush(pen.Brush, rect.Size))
using (var wrapper = CreateBrush(pen.Brush, rect))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (wrapper.PlatformBrush != null)
@ -360,7 +360,7 @@ namespace Avalonia.Direct2D1.Media
if (brush != null)
{
using (var b = CreateBrush(brush, rect.Size))
using (var b = CreateBrush(brush, rect))
{
if (b.PlatformBrush != null)
{
@ -376,7 +376,7 @@ namespace Avalonia.Direct2D1.Media
if (pen?.Brush != null)
{
using (var wrapper = CreateBrush(pen.Brush, rect.Size))
using (var wrapper = CreateBrush(pen.Brush, rect))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (wrapper.PlatformBrush != null)
@ -399,7 +399,7 @@ namespace Avalonia.Direct2D1.Media
/// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IBrush foreground, IGlyphRunImpl glyphRun)
{
using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size))
using (var brush = CreateBrush(foreground, glyphRun.Bounds))
{
var immutableGlyphRun = (GlyphRunImpl)glyphRun;
@ -516,9 +516,9 @@ namespace Avalonia.Direct2D1.Media
/// Creates a Direct2D brush wrapper for a Avalonia brush.
/// </summary>
/// <param name="brush">The avalonia brush.</param>
/// <param name="destinationSize">The size of the brush's target area.</param>
/// <param name="destinationRect">The size of the brush's target area.</param>
/// <returns>The Direct2D brush wrapper.</returns>
public BrushImpl CreateBrush(IBrush brush, Size destinationSize)
public BrushImpl CreateBrush(IBrush brush, Rect destinationRect)
{
var solidColorBrush = brush as ISolidColorBrush;
var linearGradientBrush = brush as ILinearGradientBrush;
@ -534,11 +534,11 @@ namespace Avalonia.Direct2D1.Media
}
else if (linearGradientBrush != null)
{
return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationSize);
return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationRect);
}
else if (radialGradientBrush != null)
{
return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize);
return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationRect);
}
else if (conicGradientBrush != null)
{
@ -551,7 +551,7 @@ namespace Avalonia.Direct2D1.Media
imageBrush,
_deviceContext,
(BitmapImpl)imageBrush.Source.Bitmap.Item,
destinationSize);
destinationRect);
}
else if (sceneBrush != null || sceneBrushContent != null)
{
@ -585,7 +585,7 @@ namespace Avalonia.Direct2D1.Media
sceneBrushContent.Brush,
_deviceContext,
new D2DBitmapImpl(intermediate.Bitmap.QueryInterface<Bitmap1>()),
destinationSize);
destinationRect);
}
}
@ -634,7 +634,7 @@ namespace Avalonia.Direct2D1.Media
ContentBounds = PrimitiveExtensions.RectangleInfinite,
MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
Opacity = 1,
OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush
OpacityBrush = CreateBrush(mask, bounds).PlatformBrush
};
var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext);
_deviceContext.PushLayer(ref parameters, layer);

17
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@ -13,11 +13,15 @@ namespace Avalonia.Direct2D1.Media
ITileBrush brush,
SharpDX.Direct2D1.RenderTarget target,
BitmapImpl bitmap,
Size targetSize)
Rect destinationRect)
{
var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height);
var calc = new TileBrushCalculator(brush, bitmap.PixelSize.ToSizeWithDpi(dpi), targetSize);
var calc = new TileBrushCalculator(brush, bitmap.PixelSize.ToSizeWithDpi(dpi), destinationRect.Size);
Vector brushOffset = default;
if (brush.DestinationRect.Unit == RelativeUnit.Relative)
brushOffset = new Vector(destinationRect.X, destinationRect.Y);
if (!calc.NeedsIntermediate)
{
_bitmap = bitmap.GetDirect2DBitmap(target);
@ -25,7 +29,7 @@ namespace Avalonia.Direct2D1.Media
target,
_bitmap.Value,
GetBitmapBrushProperties(brush),
GetBrushProperties(brush, calc.DestinationRect));
GetBrushProperties(brush, calc.DestinationRect, brushOffset));
}
else
{
@ -35,7 +39,7 @@ namespace Avalonia.Direct2D1.Media
target,
intermediate.Bitmap,
GetBitmapBrushProperties(brush),
GetBrushProperties(brush, calc.DestinationRect));
GetBrushProperties(brush, calc.DestinationRect, brushOffset));
}
}
}
@ -57,13 +61,16 @@ namespace Avalonia.Direct2D1.Media
};
}
private static BrushProperties GetBrushProperties(ITileBrush brush, Rect destinationRect)
private static BrushProperties GetBrushProperties(ITileBrush brush, Rect destinationRect, Vector offset)
{
var tileTransform =
brush.TileMode != TileMode.None ?
Matrix.CreateTranslation(destinationRect.X, destinationRect.Y) :
Matrix.Identity;
if (offset != default)
tileTransform = Matrix.CreateTranslation(offset);
return new BrushProperties
{
Opacity = (float)brush.Opacity,

6
src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public LinearGradientBrushImpl(
ILinearGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
Size destinationSize)
Rect destinationRect)
{
if (brush.GradientStops.Count == 0)
{
@ -21,8 +21,8 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
var startPoint = brush.StartPoint.ToPixels(destinationSize);
var endPoint = brush.EndPoint.ToPixels(destinationSize);
var startPoint = brush.StartPoint.ToPixels(destinationRect);
var endPoint = brush.EndPoint.ToPixels(destinationRect);
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,

11
src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public RadialGradientBrushImpl(
IRadialGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
Size destinationSize)
Rect destinationRect)
{
if (brush.GradientStops.Count == 0)
{
@ -21,12 +21,11 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
var centerPoint = brush.Center.ToPixels(destinationSize);
var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint;
var centerPoint = brush.Center.ToPixels(destinationRect);
var gradientOrigin = brush.GradientOrigin.ToPixels(destinationRect) - centerPoint;
// Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property
var radiusX = brush.Radius * destinationSize.Width;
var radiusY = brush.Radius * destinationSize.Height;
var radiusX = brush.RadiusX.ToValue(destinationRect.Width);
var radiusY = brush.RadiusY.ToValue(destinationRect.Height);
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,

32
tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs

@ -215,5 +215,37 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public DrawnControl(Action<DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(context);
}
[Theory(
#if !AVALONIA_SKIA
Skip = "Direct2D doesn't support conic brushes, why do we even have this file included?"
#endif
),
InlineData(false),
InlineData(true)
]
public async Task ConicGradientBrushIsProperlyMapped(bool relative)
{
var brush = new ConicGradientBrush
{
Center = relative ? RelativePoint.Center : new RelativePoint(128,128, RelativeUnit.Absolute),
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.GreenYellow, Offset = 0.2 },
new GradientStop { Color = Colors.Magenta, Offset = 0.5 },
new GradientStop { Color = Colors.Blue, Offset = 0.8 },
new GradientStop { Color = Colors.Red, Offset = 1 },
},
SpreadMethod = GradientSpreadMethod.Repeat,
Angle = 270
};
var testName =
$"{nameof(ConicGradientBrushIsProperlyMapped)}_{brush.Center.Unit}";
await RenderToFile(new RelativePointTestPrimitivesHelper(brush, !relative), testName);
CompareImages(testName);
}
}
}

22
tests/Avalonia.RenderTests/Media/ImageBrushTests.cs

@ -438,5 +438,27 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target);
CompareImages();
}
[Theory,
InlineData(false),
InlineData(true)
]
public async Task ImageBrush_Is_Properly_Mapped(bool relative)
{
var brush = new ImageBrush
{
Stretch = Stretch.Fill,
TileMode = TileMode.Tile,
DestinationRect = relative
? new RelativeRect(0, 0, 1, 1, RelativeUnit.Relative)
: new RelativeRect(0, 0, 256, 256, RelativeUnit.Absolute),
Source = new Bitmap(BitmapPath),
};
var testName =
$"{nameof(ImageBrush_Is_Properly_Mapped)}_{brush.DestinationRect.Unit}";
await RenderToFile(new RelativePointTestPrimitivesHelper(brush), testName);
CompareImages(testName);
}
}
}

24
tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs

@ -110,5 +110,29 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public DrawnControl(Action<DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(context);
}
[Theory,
InlineData(false),
InlineData(true)
]
public async Task LinearGradientBrushIsProperlyMapped(bool relative)
{
var brush = new LinearGradientBrush
{
StartPoint = relative ? new RelativePoint(0, 0, RelativeUnit.Relative) : new RelativePoint(50,0, RelativeUnit.Absolute),
EndPoint = relative ? new RelativePoint(1, 1, RelativeUnit.Relative) : new RelativePoint(150,0, RelativeUnit.Absolute),
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
},
SpreadMethod = GradientSpreadMethod.Repeat
};
var testName =
$"{nameof(LinearGradientBrushIsProperlyMapped)}_{brush.StartPoint.Unit}";
await RenderToFile(new RelativePointTestPrimitivesHelper(brush), testName);
CompareImages(testName);
}
}
}

71
tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

@ -200,5 +200,76 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public DrawnControl(Action<DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(context);
}
[Theory,
InlineData(false, false),
InlineData(false, true),
InlineData(true, false),
InlineData(true, true),
]
public async Task RadialGradientBrush_Is_Properly_Mapped(bool relative, bool moveOrigin)
{
var center = relative ? RelativePoint.Center : new RelativePoint(128, 128, RelativeUnit.Absolute);
var brush = new RadialGradientBrush
{
Center = center,
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
},
SpreadMethod = moveOrigin ? GradientSpreadMethod.Pad : GradientSpreadMethod.Repeat,
RadiusX = relative ? RelativeScalar.Middle : new RelativeScalar(128, RelativeUnit.Absolute),
RadiusY = relative ? RelativeScalar.Middle : new RelativeScalar(64, RelativeUnit.Absolute),
GradientOrigin = moveOrigin
? (relative
? new RelativePoint(0.1, 0.1, RelativeUnit.Relative)
: new RelativePoint(32, 32, RelativeUnit.Absolute))
: center
};
var testName =
$"{nameof(RadialGradientBrush_Is_Properly_Mapped)}_{(relative ? "Relative" : "Absolute")}_{(moveOrigin ? "MovedOrigin" : "CenterOrigin")}";
await RenderToFile(new RelativePointTestPrimitivesHelper(brush, !relative), testName);
CompareImages(testName);
}
[Theory(
#if !AVALONIA_SKIA
Skip = "Direct2D backend doesn't seem to support brush transforms while Direct2D is certainly capable of doing that. I'm not fixing it in this PR however"
#endif
),
InlineData(false),
InlineData(true)
]
public async Task RadialGradientBrush_With_Different_Radius_Is_Properly_Rotated(bool moveOrigin)
{
var brush = new RadialGradientBrush
{
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
},
GradientOrigin = moveOrigin ? new RelativePoint(0.1, 0.1, RelativeUnit.Relative) : RelativePoint.Center,
RadiusY = new RelativeScalar(0.25, RelativeUnit.Relative),
Transform = new RotateTransform(45),
TransformOrigin = RelativePoint.Center
};
var testName =
$"{nameof(RadialGradientBrush_With_Different_Radius_Is_Properly_Rotated)}_{(moveOrigin ? "MovedOrigin" : "CenterOrigin")}";
await RenderToFile(new Border()
{
Background = brush,
Width = 256,
Height = 256,
MinHeight = 256
}, testName);
CompareImages(testName);
}
}
}

51
tests/Avalonia.RenderTests/Media/RelativePointTestPrimitivesHelper.cs

@ -0,0 +1,51 @@
#nullable enable
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class RelativePointTestPrimitivesHelper : Control
{
private readonly IBrush? _brush;
private readonly bool _shadow;
private readonly IPen _line;
private static readonly Geometry s_Geometry = Geometry.Parse("m 80 200 c 40 20 150 -40 160 0 l 0 30 c -40 -30 -160 10 -160 -30 z");
public RelativePointTestPrimitivesHelper(IBrush? brush, bool shadow = false)
{
_brush = brush;
_shadow = shadow;
if (brush != null)
_line = new Pen(brush, 10);
MinHeight = MaxHeight = Height = MinWidth = MaxWidth = Width = 256;
}
public override void Render(DrawingContext context)
{
if (_shadow)
{
var full = new Rect(default, Bounds.Size);
context.DrawRectangle(Brushes.White, null, full);
using (context.PushOpacity(0.3))
context.DrawRectangle(_brush, null, full);
}
context.DrawRectangle(_brush, null, new Rect(20, 20, 200, 60));
context.DrawEllipse(_brush, null, new Rect(40, 100, 200, 20));
context.DrawLine(_line, new Point(60, 140), new Point(240, 160));
context.DrawGeometry(_brush, null, s_Geometry);
base.Render(context);
}
}
}

23
tests/Avalonia.RenderTests/Media/VisualBrushTests.cs

@ -687,5 +687,28 @@ namespace Avalonia.Direct2D1.RenderTests.Media
await RenderToFile(target, dpi: 192);
CompareImages();
}
[Theory,
InlineData(false),
InlineData(true)
]
public async Task VisualBrush_Is_Properly_Mapped(bool relative)
{
var brush = new VisualBrush()
{
Stretch = Stretch.Fill,
TileMode = TileMode.Tile,
DestinationRect = relative
? new RelativeRect(0, 0, 1, 1, RelativeUnit.Relative)
: new RelativeRect(0, 0, 256, 256, RelativeUnit.Absolute),
Visual = Visual
};
var testName =
$"{nameof(VisualBrush_Is_Properly_Mapped)}_{brush.DestinationRect.Unit}";
await RenderToFile(new RelativePointTestPrimitivesHelper(brush), testName);
CompareImages(testName);
}
}
}

BIN
tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
tests/TestFiles/Direct2D1/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_CenterOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_MovedOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_CenterOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_MovedOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrushIsProperlyMapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Is_Properly_Mapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrushIsProperlyMapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_CenterOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Absolute_MovedOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_CenterOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Is_Properly_Mapped_Relative_MovedOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_With_Different_Radius_Is_Properly_Rotated_CenterOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_With_Different_Radius_Is_Properly_Rotated_MovedOrigin.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Absolute.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Is_Properly_Mapped_Relative.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Loading…
Cancel
Save