Skip to content

Commit d3b61cd

Browse files
authored
[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
1 parent 57c8595 commit d3b61cd

File tree

4 files changed

+160
-116
lines changed

4 files changed

+160
-116
lines changed

src/Skia/Avalonia.Skia/DrawingContextImpl.cs

Lines changed: 134 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -886,146 +886,164 @@ private static void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect t
886886
switch (gradientBrush)
887887
{
888888
case ILinearGradientBrush linearGradient:
889-
{
890-
var start = linearGradient.StartPoint.ToPixels(targetRect).ToSKPoint();
891-
var end = linearGradient.EndPoint.ToPixels(targetRect).ToSKPoint();
892-
893-
// would be nice to cache these shaders possibly?
894-
if (linearGradient.Transform is null)
895889
{
896-
using (var shader =
897-
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
890+
var start = linearGradient.StartPoint.ToPixels(targetRect).ToSKPoint();
891+
var end = linearGradient.EndPoint.ToPixels(targetRect).ToSKPoint();
892+
893+
// would be nice to cache these shaders possibly?
894+
if (linearGradient.Transform is null)
898895
{
899-
paintWrapper.Paint.Shader = shader;
896+
using (var shader =
897+
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
898+
{
899+
paintWrapper.Paint.Shader = shader;
900+
}
900901
}
901-
}
902-
else
903-
{
904-
var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
905-
var offset = Matrix.CreateTranslation(transformOrigin);
906-
var transform = (-offset) * linearGradient.Transform.Value * (offset);
907-
908-
using (var shader =
909-
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
902+
else
910903
{
911-
paintWrapper.Paint.Shader = shader;
912-
}
913-
}
904+
var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
905+
var offset = Matrix.CreateTranslation(transformOrigin);
906+
var transform = (-offset) * linearGradient.Transform.Value * (offset);
914907

915-
break;
916-
}
917-
case IRadialGradientBrush radialGradient:
918-
{
919-
var centerPoint = radialGradient.Center.ToPixels(targetRect);
920-
var center = centerPoint.ToSKPoint();
921-
922-
var radiusX = (radialGradient.RadiusX.ToValue(targetRect.Width));
923-
var radiusY = (radialGradient.RadiusY.ToValue(targetRect.Height));
908+
using (var shader =
909+
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
910+
{
911+
paintWrapper.Paint.Shader = shader;
912+
}
913+
}
924914

925-
var originPoint = radialGradient.GradientOrigin.ToPixels(targetRect);
926-
927-
Matrix? transform = null;
928-
929-
if (radiusX != radiusY)
930-
transform =
931-
Matrix.CreateTranslation(-centerPoint)
932-
* Matrix.CreateScale(1, radiusY / radiusX)
933-
* Matrix.CreateTranslation(centerPoint);
934-
935-
936-
if (radialGradient.Transform != null)
937-
{
938-
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetRect);
939-
var offset = Matrix.CreateTranslation(transformOrigin);
940-
var brushTransform = (-offset) * radialGradient.Transform.Value * (offset);
941-
transform = transform.HasValue ? transform * brushTransform : brushTransform;
915+
break;
942916
}
943-
944-
if (originPoint.Equals(centerPoint))
917+
case IRadialGradientBrush radialGradient:
945918
{
946-
// when the origin is the same as the center the Skia RadialGradient acts the same as D2D
947-
using (var shader =
948-
transform.HasValue
949-
? SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode,
950-
transform.Value.ToSKMatrix())
951-
: SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode)
952-
)
919+
var centerPoint = radialGradient.Center.ToPixels(targetRect);
920+
var center = centerPoint.ToSKPoint();
921+
922+
var radiusX = (radialGradient.RadiusX.ToValue(targetRect.Width));
923+
var radiusY = (radialGradient.RadiusY.ToValue(targetRect.Height));
924+
925+
var originPoint = radialGradient.GradientOrigin.ToPixels(targetRect);
926+
927+
Matrix? transform = null;
928+
929+
if (radiusX != radiusY)
930+
transform =
931+
Matrix.CreateTranslation(-centerPoint)
932+
* Matrix.CreateScale(1, radiusY / radiusX)
933+
* Matrix.CreateTranslation(centerPoint);
934+
935+
936+
if (radialGradient.Transform != null)
953937
{
954-
paintWrapper.Paint.Shader = shader;
938+
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetRect);
939+
var offset = Matrix.CreateTranslation(transformOrigin);
940+
var brushTransform = (-offset) * radialGradient.Transform.Value * (offset);
941+
transform = transform.HasValue ? transform * brushTransform : brushTransform;
955942
}
956-
}
957-
else
958-
{
959-
// when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D
960943

961-
if (radiusX != radiusY)
962-
// Adjust the origin point for radiusX/Y transformation by reversing it
963-
originPoint = originPoint.WithY(
964-
(originPoint.Y - centerPoint.Y) * radiusX / radiusY + centerPoint.Y);
965-
966-
var origin = originPoint.ToSKPoint();
967-
968-
// reverse the order of the stops to match D2D
969-
var reversedColors = new SKColor[stopColors.Length];
970-
Array.Copy(stopColors, reversedColors, stopColors.Length);
971-
Array.Reverse(reversedColors);
972-
973-
// and then reverse the reference point of the stops
974-
var reversedStops = new float[stopOffsets.Length];
975-
for (var i = 0; i < stopOffsets.Length; i++)
944+
if (originPoint.Equals(centerPoint))
976945
{
977-
reversedStops[i] = stopOffsets[i];
978-
if (reversedStops[i] > 0 && reversedStops[i] < 1)
946+
// when the origin is the same as the center the Skia RadialGradient acts the same as D2D
947+
using (var shader =
948+
transform.HasValue
949+
? SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode,
950+
transform.Value.ToSKMatrix())
951+
: SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode)
952+
)
979953
{
980-
reversedStops[i] = Math.Abs(1 - stopOffsets[i]);
954+
paintWrapper.Paint.Shader = shader;
981955
}
982956
}
983-
984-
// compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
985-
using (var shader = SKShader.CreateCompose(
986-
SKShader.CreateColor(reversedColors[0]),
987-
transform.HasValue
988-
? SKShader.CreateTwoPointConicalGradient(center, (float)radiusX, origin, 0,
989-
reversedColors, reversedStops, tileMode, transform.Value.ToSKMatrix())
990-
: SKShader.CreateTwoPointConicalGradient(center, (float)radiusX, origin, 0,
991-
reversedColors, reversedStops, tileMode)
992-
993-
)
994-
)
957+
else
995958
{
996-
paintWrapper.Paint.Shader = shader;
959+
// when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D
960+
if (radiusX != radiusY)
961+
// Adjust the origin point for radiusX/Y transformation by reversing it
962+
originPoint = originPoint.WithY(
963+
(originPoint.Y - centerPoint.Y) * radiusX / radiusY + centerPoint.Y);
964+
965+
var origin = originPoint.ToSKPoint();
966+
var endOffset = 0.0;
967+
968+
// and then reverse the reference point of the stops
969+
var reversedStops = new float[stopOffsets.Length];
970+
971+
for (var i = 0; i < stopOffsets.Length; i++)
972+
{
973+
var offset = stopOffsets[i];
974+
if (endOffset < offset)
975+
{
976+
endOffset = offset;
977+
}
978+
reversedStops[i] = offset;
979+
if (reversedStops[i] > 0 && reversedStops[i] < 1)
980+
{
981+
reversedStops[i] = Math.Abs(1 - offset);
982+
}
983+
}
984+
985+
var start = origin;
986+
var radiusStart = 0f;
987+
var end = center;
988+
var radiusEnd = (float)radiusX;
989+
var reverse = MathUtilities.AreClose(1, endOffset);
990+
991+
if (reverse)
992+
{
993+
(start, radiusStart, end, radiusEnd) = (end, radiusEnd, start, radiusStart);
994+
995+
// reverse the order of the stops to match D2D
996+
var reversedColors = new SKColor[stopColors.Length];
997+
Array.Copy(stopColors, reversedColors, stopColors.Length);
998+
Array.Reverse(reversedColors);
999+
stopColors = reversedColors;
1000+
stopOffsets = reversedStops;
1001+
}
1002+
1003+
// compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
1004+
using (var shader = SKShader.CreateCompose(
1005+
SKShader.CreateColor(stopColors[0]),
1006+
transform.HasValue
1007+
? SKShader.CreateTwoPointConicalGradient(start, radiusStart, end, radiusEnd,
1008+
stopColors, stopOffsets, tileMode, transform.Value.ToSKMatrix())
1009+
: SKShader.CreateTwoPointConicalGradient(start, radiusStart, end, radiusEnd,
1010+
stopColors, stopOffsets, tileMode)
1011+
)
1012+
)
1013+
{
1014+
paintWrapper.Paint.Shader = shader;
1015+
}
9971016
}
998-
}
9991017

1000-
break;
1001-
}
1018+
break;
1019+
}
10021020
case IConicGradientBrush conicGradient:
1003-
{
1004-
var center = conicGradient.Center.ToPixels(targetRect).ToSKPoint();
1021+
{
1022+
var center = conicGradient.Center.ToPixels(targetRect).ToSKPoint();
10051023

1006-
// Skia's default is that angle 0 is from the right hand side of the center point
1007-
// but we are matching CSS where the vertical point above the center is 0.
1008-
var angle = (float)(conicGradient.Angle - 90);
1009-
var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
1024+
// Skia's default is that angle 0 is from the right hand side of the center point
1025+
// but we are matching CSS where the vertical point above the center is 0.
1026+
var angle = (float)(conicGradient.Angle - 90);
1027+
var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
10101028

1011-
if (conicGradient.Transform is { })
1012-
{
1013-
1014-
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
1015-
var offset = Matrix.CreateTranslation(transformOrigin);
1016-
var transform = (-offset) * conicGradient.Transform.Value * (offset);
1029+
if (conicGradient.Transform is { })
1030+
{
10171031

1018-
rotation = rotation.PreConcat(transform.ToSKMatrix());
1019-
}
1032+
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
1033+
var offset = Matrix.CreateTranslation(transformOrigin);
1034+
var transform = (-offset) * conicGradient.Transform.Value * (offset);
10201035

1021-
using (var shader =
1022-
SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
1023-
{
1024-
paintWrapper.Paint.Shader = shader;
1025-
}
1036+
rotation = rotation.PreConcat(transform.ToSKMatrix());
1037+
}
10261038

1027-
break;
1028-
}
1039+
using (var shader =
1040+
SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
1041+
{
1042+
paintWrapper.Paint.Shader = shader;
1043+
}
1044+
1045+
break;
1046+
}
10291047
}
10301048
}
10311049

tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,32 @@ public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush")
1919
{
2020
}
2121

22+
[Fact]
23+
public async Task RadialGradientBrush_Partial_Cover()
24+
{
25+
Decorator target = new Decorator
26+
{
27+
Padding = new Thickness(8),
28+
Width = 200,
29+
Height = 200,
30+
Child = new Border
31+
{
32+
Background = new RadialGradientBrush
33+
{
34+
GradientStops =
35+
{
36+
new GradientStop { Color = Colors.White, Offset = 0 },
37+
new GradientStop { Color = Color.Parse("#00DD00"), Offset = 0.7 }
38+
},
39+
GradientOrigin = new RelativePoint(0.7, 0.15, RelativeUnit.Relative)
40+
}
41+
}
42+
};
43+
44+
await RenderToFile(target);
45+
CompareImages();
46+
}
47+
2248
[Fact]
2349
public async Task RadialGradientBrush_RedBlue()
2450
{
Loading

0 commit comments

Comments
 (0)