* Apply tile brush transforms once the tile sits in target space Brush.Transform was concatenated into the shader matrix before the translation that moves the tile onto the painted area, so it acted in the tile's own space instead of the target's. A brush transform on a fill away from the origin - the common case, since a relative viewport resolves against the fill's bounds - came out displaced, and WPF, whose behaviour these brushes follow, disagrees. Both tile paths now place the tile first and let the transform act on the result. Where the placement is identity, which is what the existing goldens cover, the matrices are unchanged. The cross suite gains an image brush so the non-scalable path is covered too, plus a smooth ramp fixture: the star line drawing that was the only image asset survives resampling as sparse speckle, which the comparison metric cannot tell apart from a geometry error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Apply the conic gradient transform after its angle The sweep shader carried Angle as its local matrix and pre-concatenated Brush.Transform onto it, so the transform acted on the raw sweep and the angle then turned the result. Every other brush bakes its intrinsic geometry in first and lets the brush transform act on the finished pattern. The visible effect was that a translation moved the gradient in a direction rotated by Angle - 90 instead of the direction asked for. A sweep gradient is fully determined by its centre and angle, so the golden here is checked against the brush with its centre moved by the same offset and no transform: the two renders are pixel-identical, while the previous order is 0.169 rmse away against a 0.022 tolerance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Add RelativeTransform to every brush A brush transform expressed in the unit space of the painted bounds, applied before the absolute Transform - WPF's Brush.RelativeTransform. It lets one brush express a bounds-dependent transform, an SVG gradientTransform in objectBoundingBox units for instance, without baking any one consumer's bounds into a matrix, which is what makes a single brush per gradient definition possible. The property sits on IBrush and Brush, so every brush kind carries it and a consumer reads it without a type test. Each immutable brush takes it through a second constructor, leaving the existing signatures alone and marking them for collapse in v13. The gradient brush animator interpolates it alongside Transform, and the composition schema gains it on both brush bases so the value reaches the render thread as a live resource rather than a snapshot. IBrush is NotClientImplementable, so the added member is suppressed for API validation the way earlier additions to those interfaces are. Nothing consumes it yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Consume RelativeTransform in the Skia backend The relative matrix is conjugated into target space at draw time - the unit square maps onto the painted rect, whose origin translation and scale wrap the matrix - and composes before the absolute brush transform. All three gradient kinds and both tile brush paths read it; a solid colour brush has nothing for it to act on, matching WPF. With no relative transform every path reduces to the previous matrices, so existing goldens stay byte-identical. The cross suite compares the result against WPF for the linear and radial gradients, a drawing brush tiled and untiled, an image brush, the composition order against Transform, one brush shared by two differently sized fills, and the solid colour no-op. Goldens under Skia/Media additionally cover the immediate renderer and the GPU backends, which the cross suite does not exercise; each was measured against its WPF counterpart before being committed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>pull/22200/head
@ -0,0 +1,123 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
using Avalonia.Base.UnitTests.Media; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Composition; |
|||
|
|||
/// <summary>
|
|||
/// <see cref="IBrush.RelativeTransform"/> transport: the value set on the client brush must reach
|
|||
/// the server-side counterpart the backend reads at draw time, for every brush kind, both through
|
|||
/// the mutable Media brushes and through the composition brushes.
|
|||
/// </summary>
|
|||
public class RelativeTransformBrushTests : ScopedTestBase |
|||
{ |
|||
private static readonly Matrix s_matrix = Matrix.CreateRotation(0.5) * Matrix.CreateTranslation(0.25, 0.5); |
|||
|
|||
[Fact] |
|||
public void Composition_Solid_Color_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertCompositionBrushReachesServer(c => c.CreateSolidColorBrush(Colors.Red)); |
|||
|
|||
[Fact] |
|||
public void Composition_Linear_Gradient_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertCompositionBrushReachesServer(c => c.CreateLinearGradientBrush()); |
|||
|
|||
[Fact] |
|||
public void Composition_Radial_Gradient_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertCompositionBrushReachesServer(c => c.CreateRadialGradientBrush()); |
|||
|
|||
[Fact] |
|||
public void Composition_Conic_Gradient_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertCompositionBrushReachesServer(c => c.CreateConicGradientBrush()); |
|||
|
|||
[Fact] |
|||
public void Solid_Color_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertBrushReachesServer(new SolidColorBrush(Colors.Red)); |
|||
|
|||
[Fact] |
|||
public void Linear_Gradient_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertBrushReachesServer(new LinearGradientBrush { GradientStops = { new GradientStop(Colors.Red, 0) } }); |
|||
|
|||
[Fact] |
|||
public void Radial_Gradient_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertBrushReachesServer(new RadialGradientBrush { GradientStops = { new GradientStop(Colors.Red, 0) } }); |
|||
|
|||
[Fact] |
|||
public void Conic_Gradient_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertBrushReachesServer(new ConicGradientBrush { GradientStops = { new GradientStop(Colors.Red, 0) } }); |
|||
|
|||
[Fact] |
|||
public void Image_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertBrushReachesServer(new ImageBrush()); |
|||
|
|||
[Fact] |
|||
public void Drawing_Brush_Relative_Transform_Should_Reach_The_Server() |
|||
=> AssertBrushReachesServer(new DrawingBrush()); |
|||
|
|||
[Fact] |
|||
public void Changing_Relative_Transform_Raises_Invalidated() |
|||
{ |
|||
var target = new SolidColorBrush(); |
|||
|
|||
RenderResourceTestHelper.AssertResourceInvalidation( |
|||
target, () => target.RelativeTransform = new ImmutableTransform(s_matrix)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Relative_Transform_Is_Referenced_On_The_Compositor() |
|||
{ |
|||
using var services = new CompositorTestServices(); |
|||
|
|||
var transform = new RotateTransform(45); |
|||
var brush = new SolidColorBrush(Colors.Red) { RelativeTransform = transform }; |
|||
|
|||
((ICompositionRenderResource)brush).AddRefOnCompositor(services.Compositor); |
|||
try |
|||
{ |
|||
services.RunJobs(); |
|||
Assert.NotNull(((ICompositorSerializable)transform).TryGetServer(services.Compositor)); |
|||
} |
|||
finally |
|||
{ |
|||
((ICompositionRenderResource)brush).ReleaseOnCompositor(services.Compositor); |
|||
} |
|||
|
|||
Assert.Null(((ICompositorSerializable)transform).TryGetServer(services.Compositor)); |
|||
} |
|||
|
|||
private static void AssertCompositionBrushReachesServer(Func<Compositor, CompositionBrush> factory) |
|||
{ |
|||
using var services = new CompositorTestServices(); |
|||
var brush = factory(services.Compositor); |
|||
|
|||
brush.RelativeTransform = new ImmutableTransform(s_matrix); |
|||
services.RunJobs(); |
|||
|
|||
Assert.Equal(s_matrix, ((IBrush)brush.Server).RelativeTransform!.Value); |
|||
} |
|||
|
|||
private static void AssertBrushReachesServer(Brush brush) |
|||
{ |
|||
using var services = new CompositorTestServices(); |
|||
|
|||
brush.RelativeTransform = new ImmutableTransform(s_matrix); |
|||
|
|||
var resource = (ICompositionRenderResource<IBrush>)brush; |
|||
((ICompositionRenderResource)brush).AddRefOnCompositor(services.Compositor); |
|||
try |
|||
{ |
|||
services.RunJobs(); |
|||
|
|||
var server = resource.GetForCompositor(services.Compositor); |
|||
Assert.Equal(s_matrix, server.RelativeTransform!.Value); |
|||
} |
|||
finally |
|||
{ |
|||
((ICompositionRenderResource)brush).ReleaseOnCompositor(services.Compositor); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
using Avalonia.Animation.Animators; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
using Avalonia.Platform; |
|||
using Avalonia.UnitTests; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Media; |
|||
|
|||
/// <summary>
|
|||
/// <see cref="IBrush.RelativeTransform"/> survives the snapshot every mutable brush takes of itself
|
|||
/// when it is handed to a consumer that needs an immutable brush.
|
|||
/// </summary>
|
|||
public class RelativeTransformBrushTests |
|||
{ |
|||
private static readonly Matrix s_matrix = Matrix.CreateRotation(0.5) * Matrix.CreateTranslation(0.25, 0.5); |
|||
|
|||
[Fact] |
|||
public void Solid_Color_Brush_ToImmutable_Carries_Relative_Transform() |
|||
=> AssertCarriedToImmutable(new SolidColorBrush(Colors.Red)); |
|||
|
|||
[Fact] |
|||
public void Linear_Gradient_Brush_ToImmutable_Carries_Relative_Transform() |
|||
=> AssertCarriedToImmutable(new LinearGradientBrush()); |
|||
|
|||
[Fact] |
|||
public void Radial_Gradient_Brush_ToImmutable_Carries_Relative_Transform() |
|||
=> AssertCarriedToImmutable(new RadialGradientBrush()); |
|||
|
|||
[Fact] |
|||
public void Conic_Gradient_Brush_ToImmutable_Carries_Relative_Transform() |
|||
=> AssertCarriedToImmutable(new ConicGradientBrush()); |
|||
|
|||
[Fact] |
|||
public void Image_Brush_ToImmutable_Carries_Relative_Transform() |
|||
=> AssertCarriedToImmutable(new ImageBrush()); |
|||
|
|||
[Fact] |
|||
public void Scene_Brush_Content_Carries_Relative_Transform() |
|||
{ |
|||
using var app = UnitTestApplication.Start(new TestServices(renderInterface: Mock.Of<IPlatformRenderInterface>())); |
|||
|
|||
var brush = new DrawingBrush(new GeometryDrawing |
|||
{ |
|||
Geometry = new RectangleGeometry(new Rect(0, 0, 10, 10)), |
|||
Brush = Brushes.Red, |
|||
}) |
|||
{ |
|||
RelativeTransform = new ImmutableTransform(s_matrix), |
|||
}; |
|||
|
|||
using var content = ((ISceneBrush)brush).CreateContent()!; |
|||
|
|||
Assert.Equal(s_matrix, content.RelativeTransform!.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Immutable_Solid_Color_Brushes_Differing_Only_In_Relative_Transform_Are_Not_Equal() |
|||
{ |
|||
var first = new ImmutableSolidColorBrush(Colors.Red); |
|||
var second = new ImmutableSolidColorBrush(Colors.Red, 1, null, new ImmutableTransform(s_matrix)); |
|||
|
|||
Assert.NotEqual(first, second); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Animating_A_Gradient_Brush_Keeps_The_Relative_Transform() |
|||
{ |
|||
var animator = new GradientBrushAnimator(); |
|||
var from = new ImmutableLinearGradientBrush( |
|||
[], 1, null, null, GradientSpreadMethod.Pad, null, null, new ImmutableTransform(s_matrix)); |
|||
var to = new ImmutableLinearGradientBrush([]); |
|||
|
|||
var interpolated = animator.Interpolate(0.5, from, to); |
|||
|
|||
Assert.Equal(s_matrix, interpolated!.RelativeTransform!.Value); |
|||
} |
|||
|
|||
private static void AssertCarriedToImmutable(Brush brush) |
|||
{ |
|||
brush.RelativeTransform = new ImmutableTransform(s_matrix); |
|||
|
|||
var immutable = brush.ToImmutable(); |
|||
|
|||
Assert.Equal(s_matrix, immutable.RelativeTransform!.Value); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 189 B |
@ -0,0 +1,186 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using Avalonia.Media; |
|||
using CrossUI; |
|||
using Xunit; |
|||
|
|||
#if AVALONIA_SKIA
|
|||
namespace Avalonia.Skia.RenderTests; |
|||
#else
|
|||
namespace Avalonia.RenderTests.WpfCompare; |
|||
#endif
|
|||
|
|||
public class CrossRelativeTransformBrushTests : CrossTestBase |
|||
{ |
|||
public CrossRelativeTransformBrushTests() : base("Media/RelativeTransformBrush") |
|||
{ |
|||
} |
|||
|
|||
// A rotation about an off-centre point of the unit square: bounds-dependent once conjugated
|
|||
// into target space, and asymmetric enough to show on a centre-symmetric radial gradient,
|
|||
// where a rotation about the centre would be invisible.
|
|||
private static readonly Matrix s_unitRotation = |
|||
Matrix.CreateTranslation(-0.3, -0.7) |
|||
* Matrix.CreateRotation(Matrix.ToRadians(30)) |
|||
* Matrix.CreateTranslation(0.3, 0.7); |
|||
|
|||
[CrossFact] |
|||
public void Linear_Gradient_Relative_Transform_Should_Work_As_Expected() |
|||
{ |
|||
RenderAndCompare(Scene(ctx => |
|||
ctx.DrawRectangle(Linear(relative: s_unitRotation), null, new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
[CrossFact] |
|||
public void Radial_Gradient_Relative_Transform_Should_Work_As_Expected() |
|||
{ |
|||
RenderAndCompare(Scene(ctx => |
|||
ctx.DrawRectangle(Radial(relative: s_unitRotation), null, new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
[CrossFact] |
|||
public void Relative_Transform_Should_Apply_Before_Transform() |
|||
{ |
|||
// A scale rather than a translation: it does not commute with the relative rotation, so the
|
|||
// two orders are far enough apart to be told apart by the comparison.
|
|||
RenderAndCompare(Scene(ctx => |
|||
ctx.DrawRectangle( |
|||
Linear(relative: s_unitRotation, absolute: Matrix.CreateScale(0.4, 0.4)), |
|||
null, |
|||
new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
[CrossFact] |
|||
public void Relative_Transform_Should_Follow_The_Bounds_Of_Each_Fill() |
|||
{ |
|||
// One brush instance, two differently sized rects: the unit space is each rect's own, so
|
|||
// the two fills must differ in shape.
|
|||
RenderAndCompare(Scene(ctx => |
|||
{ |
|||
var shared = Linear(relative: s_unitRotation); |
|||
ctx.DrawRectangle(shared, null, new Rect(40, 30, 120, 60)); |
|||
ctx.DrawRectangle(shared, null, new Rect(30, 110, 60, 90)); |
|||
})); |
|||
} |
|||
|
|||
[CrossFact] |
|||
public void Solid_Color_Relative_Transform_Should_Be_Ignored() |
|||
{ |
|||
RenderAndCompare(Scene(ctx => |
|||
ctx.DrawRectangle( |
|||
new CrossSolidColorBrush(Colors.Crimson) { RelativeTransform = s_unitRotation }, |
|||
null, |
|||
new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
[CrossFact] |
|||
public void Drawing_Brush_Relative_Transform_Should_Work_As_Expected() |
|||
{ |
|||
RenderAndCompare(Scene(ctx => |
|||
ctx.DrawRectangle(Checkerboard(relative: s_unitRotation), null, new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
[CrossFact] |
|||
public void Tiled_Drawing_Brush_Relative_Transform_Should_Work_As_Expected() |
|||
{ |
|||
var brush = Checkerboard(relative: s_unitRotation); |
|||
brush.TileMode = TileMode.Tile; |
|||
brush.Viewport = new Rect(0, 0, 30, 30); |
|||
brush.ViewportUnits = BrushMappingMode.Absolute; |
|||
|
|||
RenderAndCompare(Scene(ctx => ctx.DrawRectangle(brush, null, new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
[CrossFact] |
|||
public void Image_Brush_Relative_Transform_Should_Work_As_Expected() |
|||
{ |
|||
RenderAndCompare(Scene(ctx => |
|||
ctx.DrawRectangle( |
|||
new CrossImageBrush { Path = RampImagePath, RelativeTransform = s_unitRotation }, |
|||
null, |
|||
new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
// A smooth two-axis ramp rather than a line drawing: every pixel carries position, so a
|
|||
// misplaced transform shows up as a colour shift over the whole fill instead of hiding in the
|
|||
// resampling noise the two backends produce differently along hard edges.
|
|||
[CrossFact] |
|||
public void Tiled_Image_Brush_Relative_And_Absolute_Transform_Should_Work_As_Expected() |
|||
{ |
|||
// Tiling and an absolute viewport put the tile offset and the destination translate into
|
|||
// the shader matrix, so this is where the slot the two brush transforms occupy matters.
|
|||
RenderAndCompare(Scene(ctx => ctx.DrawRectangle( |
|||
new CrossImageBrush |
|||
{ |
|||
Path = RampImagePath, |
|||
TileMode = TileMode.Tile, |
|||
Viewport = new Rect(0, 0, 40, 40), |
|||
ViewportUnits = BrushMappingMode.Absolute, |
|||
RelativeTransform = s_unitRotation, |
|||
Transform = Matrix.CreateScale(0.6, 0.6) |
|||
}, |
|||
null, |
|||
new Rect(40, 70, 120, 60)))); |
|||
} |
|||
|
|||
private static string RampImagePath => Path.Join( |
|||
Path.GetDirectoryName(typeof(CrossRelativeTransformBrushTests).Assembly.Location), |
|||
"Assets", |
|||
"Ramp64.png"); |
|||
|
|||
private static CrossControl Scene(Action<ICrossDrawingContext> render) => |
|||
new CrossFuncControl(render) |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = new CrossSolidColorBrush(Colors.White) |
|||
}; |
|||
|
|||
private static CrossLinearGradientBrush Linear(Matrix? relative = null, Matrix? absolute = null) => |
|||
new() |
|||
{ |
|||
GradientStops = { new GradientStop(Colors.Red, 0), new GradientStop(Colors.Blue, 1) }, |
|||
StartPoint = new Point(0, 0), |
|||
EndPoint = new Point(1, 0), |
|||
RelativeTransform = relative, |
|||
Transform = absolute |
|||
}; |
|||
|
|||
private static CrossRadialGradientBrush Radial(Matrix? relative = null, Matrix? absolute = null) => |
|||
new() |
|||
{ |
|||
GradientStops = { new GradientStop(Colors.Red, 0), new GradientStop(Colors.Blue, 1) }, |
|||
Center = new Point(0.5, 0.5), |
|||
GradientOrigin = new Point(0.5, 0.5), |
|||
RadiusX = 0.5, |
|||
RadiusY = 0.5, |
|||
RelativeTransform = relative, |
|||
Transform = absolute |
|||
}; |
|||
|
|||
private static CrossDrawingBrush Checkerboard(Matrix? relative = null, Matrix? absolute = null) => |
|||
new() |
|||
{ |
|||
Drawing = new CrossDrawingGroup |
|||
{ |
|||
Children = new List<CrossDrawing> |
|||
{ |
|||
new CrossGeometryDrawing(new CrossRectangleGeometry(new Rect(0, 0, 20, 20))) |
|||
{ |
|||
Brush = new CrossSolidColorBrush(Colors.Crimson) |
|||
}, |
|||
new CrossGeometryDrawing(new CrossRectangleGeometry(new Rect(0, 0, 10, 10))) |
|||
{ |
|||
Brush = new CrossSolidColorBrush(Colors.MidnightBlue) |
|||
}, |
|||
new CrossGeometryDrawing(new CrossRectangleGeometry(new Rect(10, 10, 10, 10))) |
|||
{ |
|||
Brush = new CrossSolidColorBrush(Colors.MidnightBlue) |
|||
} |
|||
} |
|||
}, |
|||
RelativeTransform = relative, |
|||
Transform = absolute |
|||
}; |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Skia.RenderTests; |
|||
|
|||
/// <summary>
|
|||
/// <see cref="IBrush.RelativeTransform"/> applies in the unit
|
|||
/// space of the painted bounds, before the absolute <see cref="IBrush.Transform"/>,
|
|||
/// so one brush instance serves consumers of different sizes.
|
|||
/// </summary>
|
|||
public class RelativeTransformBrushTests : TestBase |
|||
{ |
|||
private static readonly ImmutableGradientStop[] s_stops = |
|||
{ |
|||
new(0, Colors.Red), |
|||
new(1, Colors.Blue), |
|||
}; |
|||
|
|||
// A rotation about an off-center unit-space point: bounds-dependent once
|
|||
// conjugated, and asymmetric enough that even a center-symmetric radial
|
|||
// gradient shows it (a rotation about the center would be invisible there).
|
|||
private static readonly Matrix s_unitMatrix = |
|||
Matrix.CreateTranslation(-0.3, -0.7) |
|||
* Matrix.CreateRotation(Matrix.ToRadians(30)) |
|||
* Matrix.CreateTranslation(0.3, 0.7); |
|||
|
|||
public RelativeTransformBrushTests() |
|||
: base(@"Media\RelativeTransformBrush") |
|||
{ |
|||
} |
|||
|
|||
private static Canvas Scene(params Control[] children) |
|||
{ |
|||
var canvas = new Canvas |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = Brushes.White, |
|||
}; |
|||
|
|||
foreach (var child in children) |
|||
canvas.Children.Add(child); |
|||
|
|||
return canvas; |
|||
} |
|||
|
|||
private static Border Filled(IBrush brush, double left, double top, double width, double height) => |
|||
new() |
|||
{ |
|||
Background = brush, |
|||
Width = width, |
|||
Height = height, |
|||
[Canvas.LeftProperty] = left, |
|||
[Canvas.TopProperty] = top, |
|||
}; |
|||
|
|||
[Fact] |
|||
public async Task Linear_Gradient_Rotated_In_Unit_Space() |
|||
{ |
|||
var brush = new ImmutableLinearGradientBrush( |
|||
s_stops, 1, null, null, GradientSpreadMethod.Pad, null, null, |
|||
new ImmutableTransform(s_unitMatrix)); |
|||
|
|||
await RenderToFile(Scene(Filled(brush, 40, 70, 120, 60))); |
|||
CompareImages(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Radial_Gradient_Rotated_In_Unit_Space() |
|||
{ |
|||
var brush = new ImmutableRadialGradientBrush( |
|||
s_stops, 1, null, null, GradientSpreadMethod.Pad, null, null, null, null, |
|||
new ImmutableTransform(s_unitMatrix)); |
|||
|
|||
await RenderToFile(Scene(Filled(brush, 40, 70, 120, 60))); |
|||
CompareImages(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Shared_Brush_On_Two_Different_Bounds() |
|||
{ |
|||
// The gradient must follow each rect's own bounds, so the two fills
|
|||
// differ in shape while sharing one brush instance.
|
|||
var shared = new ImmutableLinearGradientBrush( |
|||
s_stops, 1, null, null, GradientSpreadMethod.Pad, null, null, |
|||
new ImmutableTransform(s_unitMatrix)); |
|||
|
|||
await RenderToFile( |
|||
Scene( |
|||
Filled(shared, 40, 30, 120, 60), |
|||
Filled(shared, 30, 110, 60, 90))); |
|||
CompareImages(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Drawing_Brush_Rotated_In_Unit_Space() |
|||
{ |
|||
var brush = new DrawingBrush(new DrawingGroup |
|||
{ |
|||
Children = |
|||
{ |
|||
new GeometryDrawing |
|||
{ |
|||
Geometry = new RectangleGeometry(new Rect(0, 0, 20, 20)), |
|||
Brush = Brushes.Crimson, |
|||
}, |
|||
new GeometryDrawing |
|||
{ |
|||
Geometry = new RectangleGeometry(new Rect(0, 0, 10, 10)), |
|||
Brush = Brushes.MidnightBlue, |
|||
}, |
|||
new GeometryDrawing |
|||
{ |
|||
Geometry = new RectangleGeometry(new Rect(10, 10, 10, 10)), |
|||
Brush = Brushes.MidnightBlue, |
|||
}, |
|||
} |
|||
}) |
|||
{ |
|||
Stretch = Stretch.Fill, |
|||
RelativeTransform = new ImmutableTransform(s_unitMatrix), |
|||
}; |
|||
|
|||
await RenderToFile(Scene(Filled(brush, 40, 70, 120, 60))); |
|||
CompareImages(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Relative_Transform_Applies_Before_Absolute() |
|||
{ |
|||
// A scale, not a translation: it does not commute with the relative
|
|||
// rotation, so applying the two the other way round moves the image far
|
|||
// enough to fail this golden. A translation stayed inside the tolerance.
|
|||
var brush = new ImmutableLinearGradientBrush( |
|||
s_stops, 1, new ImmutableTransform(Matrix.CreateScale(0.4, 0.4)), null, |
|||
GradientSpreadMethod.Pad, null, null, |
|||
new ImmutableTransform(s_unitMatrix)); |
|||
|
|||
await RenderToFile(Scene(Filled(brush, 40, 70, 120, 60))); |
|||
CompareImages(); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 657 B |
|
After Width: | Height: | Size: 657 B |
|
After Width: | Height: | Size: 657 B |
|
After Width: | Height: | Size: 670 B |
|
After Width: | Height: | Size: 670 B |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 666 B |
|
After Width: | Height: | Size: 861 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |