Browse Source

[Skia] Fix RadialGradientBrush for non center origin (#17925)

* Skia - Only reverse radial gradient stops if we cover the whole content bounds

* Skia - Only reverse radial gradient stops if we reach cover the whole content bounds

* Add some new lines
pull/18008/head
Benedikt Stebner 1 year ago
committed by GitHub
parent
commit
d3b61cd75e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 250
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  2. 26
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  3. BIN
      tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png
  4. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png

250
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -886,146 +886,164 @@ namespace Avalonia.Skia
switch (gradientBrush)
{
case ILinearGradientBrush linearGradient:
{
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)
{
using (var shader =
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
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)
{
paintWrapper.Paint.Shader = shader;
using (var shader =
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
{
paintWrapper.Paint.Shader = shader;
}
}
}
else
{
var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * linearGradient.Transform.Value * (offset);
using (var shader =
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
else
{
paintWrapper.Paint.Shader = shader;
}
}
var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * linearGradient.Transform.Value * (offset);
break;
}
case IRadialGradientBrush radialGradient:
{
var centerPoint = radialGradient.Center.ToPixels(targetRect);
var center = centerPoint.ToSKPoint();
var radiusX = (radialGradient.RadiusX.ToValue(targetRect.Width));
var radiusY = (radialGradient.RadiusY.ToValue(targetRect.Height));
using (var shader =
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
{
paintWrapper.Paint.Shader = shader;
}
}
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;
break;
}
if (originPoint.Equals(centerPoint))
case IRadialGradientBrush radialGradient:
{
// when the origin is the same as the center the Skia RadialGradient acts the same as D2D
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 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)
{
paintWrapper.Paint.Shader = shader;
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var brushTransform = (-offset) * radialGradient.Transform.Value * (offset);
transform = transform.HasValue ? transform * brushTransform : brushTransform;
}
}
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);
Array.Reverse(reversedColors);
// and then reverse the reference point of the stops
var reversedStops = new float[stopOffsets.Length];
for (var i = 0; i < stopOffsets.Length; i++)
if (originPoint.Equals(centerPoint))
{
reversedStops[i] = stopOffsets[i];
if (reversedStops[i] > 0 && reversedStops[i] < 1)
// when the origin is the same as the center the Skia RadialGradient acts the same as D2D
using (var shader =
transform.HasValue
? SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode,
transform.Value.ToSKMatrix())
: SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode)
)
{
reversedStops[i] = Math.Abs(1 - stopOffsets[i]);
paintWrapper.Paint.Shader = shader;
}
}
// compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
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)
)
)
else
{
paintWrapper.Paint.Shader = shader;
// 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();
var endOffset = 0.0;
// and then reverse the reference point of the stops
var reversedStops = new float[stopOffsets.Length];
for (var i = 0; i < stopOffsets.Length; i++)
{
var offset = stopOffsets[i];
if (endOffset < offset)
{
endOffset = offset;
}
reversedStops[i] = offset;
if (reversedStops[i] > 0 && reversedStops[i] < 1)
{
reversedStops[i] = Math.Abs(1 - offset);
}
}
var start = origin;
var radiusStart = 0f;
var end = center;
var radiusEnd = (float)radiusX;
var reverse = MathUtilities.AreClose(1, endOffset);
if (reverse)
{
(start, radiusStart, end, radiusEnd) = (end, radiusEnd, start, radiusStart);
// reverse the order of the stops to match D2D
var reversedColors = new SKColor[stopColors.Length];
Array.Copy(stopColors, reversedColors, stopColors.Length);
Array.Reverse(reversedColors);
stopColors = reversedColors;
stopOffsets = reversedStops;
}
// compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
using (var shader = SKShader.CreateCompose(
SKShader.CreateColor(stopColors[0]),
transform.HasValue
? SKShader.CreateTwoPointConicalGradient(start, radiusStart, end, radiusEnd,
stopColors, stopOffsets, tileMode, transform.Value.ToSKMatrix())
: SKShader.CreateTwoPointConicalGradient(start, radiusStart, end, radiusEnd,
stopColors, stopOffsets, tileMode)
)
)
{
paintWrapper.Paint.Shader = shader;
}
}
}
break;
}
break;
}
case IConicGradientBrush conicGradient:
{
var center = conicGradient.Center.ToPixels(targetRect).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.
var angle = (float)(conicGradient.Angle - 90);
var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
// 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.
var angle = (float)(conicGradient.Angle - 90);
var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
if (conicGradient.Transform is { })
{
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * conicGradient.Transform.Value * (offset);
if (conicGradient.Transform is { })
{
rotation = rotation.PreConcat(transform.ToSKMatrix());
}
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * conicGradient.Transform.Value * (offset);
using (var shader =
SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
{
paintWrapper.Paint.Shader = shader;
}
rotation = rotation.PreConcat(transform.ToSKMatrix());
}
break;
}
using (var shader =
SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
{
paintWrapper.Paint.Shader = shader;
}
break;
}
}
}

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

@ -19,6 +19,32 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
}
[Fact]
public async Task RadialGradientBrush_Partial_Cover()
{
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 200,
Height = 200,
Child = new Border
{
Background = new RadialGradientBrush
{
GradientStops =
{
new GradientStop { Color = Colors.White, Offset = 0 },
new GradientStop { Color = Color.Parse("#00DD00"), Offset = 0.7 }
},
GradientOrigin = new RelativePoint(0.7, 0.15, RelativeUnit.Relative)
}
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task RadialGradientBrush_RedBlue()
{

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Loading…
Cancel
Save