Skip to content

Commit

Permalink
[Skia] Fix RadialGradientBrush for non center origin (#17925)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Gillibald authored Jan 20, 2025
1 parent 57c8595 commit d3b61cd
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 116 deletions.
250 changes: 134 additions & 116 deletions src/Skia/Avalonia.Skia/DrawingContextImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -886,146 +886,164 @@ private static void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect t
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;
}
}
}

Expand Down
26 changes: 26 additions & 0 deletions tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush")
{
}

[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()
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d3b61cd

Please sign in to comment.