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) switch (gradientBrush)
{ {
case ILinearGradientBrush linearGradient: 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 = var start = linearGradient.StartPoint.ToPixels(targetRect).ToSKPoint();
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) 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
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()))
{ {
paintWrapper.Paint.Shader = shader; var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
} var offset = Matrix.CreateTranslation(transformOrigin);
} var transform = (-offset) * linearGradient.Transform.Value * (offset);
break; using (var shader =
} SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
case IRadialGradientBrush radialGradient: {
{ paintWrapper.Paint.Shader = shader;
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); break;
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;
} }
case IRadialGradientBrush radialGradient:
if (originPoint.Equals(centerPoint))
{ {
// when the origin is the same as the center the Skia RadialGradient acts the same as D2D var centerPoint = radialGradient.Center.ToPixels(targetRect);
using (var shader = var center = centerPoint.ToSKPoint();
transform.HasValue
? SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode, var radiusX = (radialGradient.RadiusX.ToValue(targetRect.Width));
transform.Value.ToSKMatrix()) var radiusY = (radialGradient.RadiusY.ToValue(targetRect.Height));
: SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode)
) 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) if (originPoint.Equals(centerPoint))
// 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++)
{ {
reversedStops[i] = stopOffsets[i]; // when the origin is the same as the center the Skia RadialGradient acts the same as D2D
if (reversedStops[i] > 0 && reversedStops[i] < 1) 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;
} }
} }
else
// 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)
)
)
{ {
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: 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 // 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. // but we are matching CSS where the vertical point above the center is 0.
var angle = (float)(conicGradient.Angle - 90); var angle = (float)(conicGradient.Angle - 90);
var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y); var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
if (conicGradient.Transform is { }) if (conicGradient.Transform is { })
{ {
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * conicGradient.Transform.Value * (offset);
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 = rotation = rotation.PreConcat(transform.ToSKMatrix());
SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation)) }
{
paintWrapper.Paint.Shader = shader;
}
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] [Fact]
public async Task RadialGradientBrush_RedBlue() 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