From 23b3a767dcde97cbcd3df2fb03bafd35df1b6942 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 2 Apr 2024 06:49:37 +0200 Subject: [PATCH] Rework tile brush calculation (#15157) --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 79 ++++++++++++++---- .../Media/DrawingContextImpl.cs | 24 ++++-- .../Media/ImageBrushImpl.cs | 8 ++ .../Media/ImageBrushTests.cs | 29 +++++++ .../Media/ImageDrawingTests.cs | 56 +++++++++++++ ...le_Small_Image_With_Transform.expected.png | Bin 0 -> 3383 bytes ..._Render_DrawingBrushTransform.expected.png | Bin 0 -> 1146 bytes ...le_Small_Image_With_Transform.expected.png | Bin 0 -> 2312 bytes ..._Render_DrawingBrushTransform.expected.png | Bin 0 -> 1188 bytes 9 files changed, 173 insertions(+), 23 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Small_Image_With_Transform.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/ImageDrawing/Should_Render_DrawingBrushTransform.expected.png create mode 100644 tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Small_Image_With_Transform.expected.png create mode 100644 tests/TestFiles/Skia/Media/ImageDrawing/Should_Render_DrawingBrushTransform.expected.png diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index eecb56e90a..493d87988b 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -1149,43 +1149,85 @@ namespace Avalonia.Skia private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content, Rect targetRect) { - var rect = content.Rect; - var contentSize = rect.Size; - if (contentSize.Width <= 0 || contentSize.Height <= 0) + var tileBrush = content.Brush; + + var contentBounds = content.Rect; + + if (contentBounds.Size.Width <= 0 || contentBounds.Size.Height <= 0) { paintWrapper.Paint.Color = SKColor.Empty; + return; } - - var tileBrush = content.Brush; - var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y); + + var brushTransform = Matrix.CreateTranslation(-contentBounds.Position); + + contentBounds = contentBounds.TransformToAABB(brushTransform); + + var destinationRect = content.Brush.DestinationRect.ToPixels(targetRect.Size); + + if (tileBrush.Stretch != Stretch.None) + { + //scale content to destination size + var scale = tileBrush.Stretch.CalculateScaling(destinationRect.Size, contentBounds.Size); + + var scaleTransform = Matrix.CreateScale(scale); + + contentBounds = contentBounds.TransformToAABB(scaleTransform); + + brushTransform *= scaleTransform; + } + + var sourceRect = tileBrush.SourceRect.ToPixels(contentBounds); + + //scale content to source size + if (contentBounds.Size != sourceRect.Size) + { + var scale = tileBrush.Stretch.CalculateScaling(sourceRect.Size, contentBounds.Size); + + var scaleTransform = Matrix.CreateScale(scale); + + contentBounds = contentBounds.TransformToAABB(scaleTransform); + + brushTransform *= scaleTransform; + } + + var transform = Matrix.Identity; if (content.Transform is not null) { var transformOrigin = content.TransformOrigin.ToPixels(targetRect); var offset = Matrix.CreateTranslation(transformOrigin); + transform = -offset * content.Transform.Value * offset; + } - transform *= -offset * content.Transform.Value * offset; + if (content.Brush.TileMode == TileMode.None) + { + brushTransform *= transform; + } + + if (tileBrush.Stretch == Stretch.None && transform == Matrix.Identity) + { + //align content + var alignmentOffset = TileBrushCalculator.CalculateTranslate(tileBrush.AlignmentX, tileBrush.AlignmentY, + contentBounds, destinationRect, Vector.One); + + brushTransform *= Matrix.CreateTranslation(alignmentOffset); } - var calc = new TileBrushCalculator(tileBrush, contentSize, targetRect.Size); - transform *= calc.IntermediateTransform; - using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi); - using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize)) + using (var ctx = pictureTarget.CreateDrawingContext(destinationRect.Size)) { - ctx.PushClip(calc.IntermediateClip); ctx.PushRenderOptions(RenderOptions); - content.Render(ctx, transform); + content.Render(ctx, brushTransform); ctx.PopRenderOptions(); - ctx.PopClip(); } using var picture = pictureTarget.GetPicture(); var paintTransform = tileBrush.TileMode != TileMode.None - ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + ? SKMatrix.CreateTranslation(-(float)destinationRect.X, -(float)destinationRect.Y) : SKMatrix.CreateIdentity(); SKShaderTileMode tileX = @@ -1204,11 +1246,16 @@ namespace Avalonia.Skia paintTransform = SKMatrix.Concat(paintTransform, SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y))); - + if (tileBrush.DestinationRect.Unit == RelativeUnit.Relative) paintTransform = paintTransform.PreConcat(SKMatrix.CreateTranslation((float)targetRect.X, (float)targetRect.Y)); + if (tileBrush.TileMode != TileMode.None) + { + paintTransform = paintTransform.PreConcat(transform.ToSKMatrix()); + } + using (var shader = picture.ToShader(tileX, tileY, paintTransform, new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height))) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index d889e394e6..c40c5e9871 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -614,6 +614,20 @@ namespace Avalonia.Direct2D1.Media var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height); var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi); + var transform = rect.TopLeft == default ? + Matrix.Identity : + Matrix.CreateTranslation(-rect.X, -rect.Y); + + var brushTransform = Matrix.Identity; + + if (sceneBrushContent.Transform != null) + { + var transformOrigin = sceneBrushContent.TransformOrigin.ToPixels(rect); + var offset = Matrix.CreateTranslation(transformOrigin); + + brushTransform = -offset * sceneBrushContent.Transform.Value * offset; + } + using (var intermediate = new BitmapRenderTarget( _deviceContext, CompatibleRenderTargetOptions.None, @@ -623,16 +637,12 @@ namespace Avalonia.Direct2D1.Media { intermediate.Clear(null); - if (sceneBrush?.Transform is not null) + if (sceneBrush?.TileMode == TileMode.None) { - var transformOrigin = sceneBrushContent.TransformOrigin.ToPixels(rect); - var offset = Matrix.CreateTranslation(transformOrigin); - - ctx.Transform = -offset * sceneBrush.Transform.Value * offset; + transform = brushTransform * transform; } - sceneBrushContent.Render(ctx, - rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); + sceneBrushContent.Render(ctx, transform); } return new ImageBrushImpl( diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index ef4d91df9d..7f595f8272 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -71,6 +71,14 @@ namespace Avalonia.Direct2D1.Media if (offset != default) tileTransform = Matrix.CreateTranslation(offset); + if (brush.Transform != null && brush.TileMode != TileMode.None) + { + var transformOrigin = brush.TransformOrigin.ToPixels(destinationRect); + var originOffset = Matrix.CreateTranslation(transformOrigin); + + tileTransform = -originOffset * brush.Transform.Value * originOffset * tileTransform; + } + return new BrushProperties { Opacity = (float)brush.Opacity, diff --git a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs index 2a49444a10..aef33bc131 100644 --- a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs @@ -521,5 +521,34 @@ namespace Avalonia.Direct2D1.RenderTests.Media CompareImages(); } + + [Fact] + public async Task ImageBrush_Tile_Small_Image_With_Transform() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Rectangle + { + Margin = new Thickness(8), + Fill = new DrawingBrush + { + DestinationRect = new RelativeRect(0,0,32,32, RelativeUnit.Absolute), + Transform = new TranslateTransform(10,10), + Stretch = Stretch.None, + TileMode = TileMode.Tile, + Drawing = new ImageDrawing + { + Rect = new Rect(0,0,32,32), + ImageSource = new Bitmap(SmallBitmapPath) + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } } } diff --git a/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs index a86113b4ef..3d593cd677 100644 --- a/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageDrawingTests.cs @@ -83,5 +83,61 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task Should_Render_DrawingBrushTransform() + { + var target = new Border + { + Width = 400, + Height = 400, + Child = new DrawingBrushTransformTest() + }; + + await RenderToFile(target); + CompareImages(); + } + + public class DrawingBrushTransformTest : Control + { + private readonly DrawingBrush _brush; + + public DrawingBrushTransformTest() + { + _brush = new DrawingBrush() + { + TileMode = TileMode.None, + SourceRect = new RelativeRect(0, 0, 50, 50, RelativeUnit.Absolute), + DestinationRect = new RelativeRect(0, 0, 1, 1, RelativeUnit.Relative), + Transform = new TranslateTransform(150, 150), + Drawing = new DrawingGroup() + { + Children = new DrawingCollection() + { + new GeometryDrawing + { + Brush = Brushes.Crimson, + Geometry = new RectangleGeometry(new(0, 0, 100, 100)) + }, + new GeometryDrawing + { + Brush = Brushes.Blue, + Geometry = new RectangleGeometry(new(20, 20, 60, 60)) + } + } + } + }; + } + + public override void Render(DrawingContext drawingContext) + { + var pop = drawingContext.PushTransform(Matrix.CreateTranslation(100, 100)); + var rc = new Rect(0, 0, 200, 200); + drawingContext.DrawRectangle(new SolidColorBrush(Colors.DimGray), null, rc); + drawingContext.DrawRectangle(_brush, null, rc); + + pop.Dispose(); + } + } } } diff --git a/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Small_Image_With_Transform.expected.png b/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Small_Image_With_Transform.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..ac400eae2ac6ad7b3b4a30b7da95ad02c626a5d2 GIT binary patch literal 3383 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 PFG#@C)z4*}Q$iB}$~DyG literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Small_Image_With_Transform.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Small_Image_With_Transform.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e7bf6187c081d6efb8bbdb69a94c41ce612881 GIT binary patch literal 2312 zcmcgudr(tn9;Gc?5SB&;7+3)>ErC`+aHS9s69vK}wI)0)h!j{?cQB$xjTjywQIH~1 z9;QST16c~lP?w-Z1QJL}i^UX4ScUK=LLdkMk`N#d60$c~N5|UT{SGrOmpm#^Q8n=?1@Im-C+~h$;343>axiy%g|oXg8s$xUudYHr6y#VvB2;Yi;WK=9$mH zbp}vc-O&D5dkQq7N|}62ux=0NopkXR1e++`XKo(ChwQQFMmd`85OW_D4T zUzYvv#Sxht($~-)BG|<9t7j`0`7YSw=Y0Bv2}8rorFrz_9-Kgk*yCpf#ippm{Yk(+ z)5B?6`l1GZhYCaZy8el%4U#8!YptD8?>R>aSK}86b@pvhYVk_^fL#U6;zMX%ln6?c zcy{B-c6KTax2cJMIPp;Tc8ykf4Nmu zM!Kq5?%<@SV>sUrJ%3OPJ(qy{Y_W&ZAaX{bP;=|TTRrP|nS5mC79F~({Y$4`Ej357 zBNOZMsh|BZV)5E0fKyHqc*X93=YO%Ib1r#>hl)&_C>t@Oms69~@iKW)30++=6B<&% z$&d)DGR?iqGZva`(^(kpp!#^ohqSXixNs@~e$YW+HT$z@it8zt`%jmaig`_#6F;IIt9w@sJ9Y*QzpH^4yAy@-41YH1dO?HD9S_j_2r5cH@Hp?&RF0b}C`f39)`ea}!b=R^wJs7>6NyvwFCg zKOgu!7YeOP-mj3Np2!B5hX!+>nsvBz;6`KFXE79wCx^sjEsbgtEQs26RqTR3<^VLO z_;ZFwkEdWO%XfFmwyUE=x72Z&T%kFo3pkQ__KYwrzBi}u9y+H;h}}EzUNv-pzT2(@ zA*hte{TF7^FG4+*Z#O*0Otrcq)+3hi@O{-#UB(Kd<6a?%hy@XyTrwnA+1s^$;EsElVJ1~dO?jUNb zrjkeqrY~Y;Akumzw_5GiNK>5SZ9`4oo=F9^6^d+JCW2dgt*A`i<;~XC)pr%P?o%`x z{;C@lLp|;+PtPh6ZX^F=;xM~(G@7+@7k>M87s!^(f*4&yMq@tJ7Iz-?-Tf2bRNL%T zytB*q!fec)Zs~eBg$KcqDwW8Aii!|7PsIH7)42Ulet6!8BTlhD_M_S^l%k`_4_%=0 z2DuzE+;Cl&DHb_<|#bB9(@95ymPh_{7s76MnK_RWmsG0UnSlgWP2-O|QRAI%P6^vt{*y0* z7)joW zDKP{$DnwEQSF40|M^!45=~~9Mlq_hlH@R>;w1(aF9#}9WdtfBG#N#st0_8 zDT35!s3rl%0GSL{k;uNAFJA+MS28yfiO#D5GV_I`hX_+rWhVG6$9IA_wxxk0WIp?u z$o?KAs|fd&V&ZYSfxv(nk;EJY&v?ZsFL=u_ZxpqGYX<(24YfU>aKON))v4#RH(N#| z847O@dfunofVy&4hd@*OKOk{K_piPT7+gbQ!AsL`sCkjZTZ`rkjfMy|)WY&#dRERf zLIRP<^1R;?a(YS8pf%fJL-2b;|7(h`Mf<{+0mEr0c^h*511Y|y+w;DmM&I}i(Y_eCSM4!_sv*CcL?#0y^=&05v;|L;b>Ch`9^pjMWzmd#!L ayAxlD10Nk+`6u{mV}uGuBN>5z%lZ#?>cf8k literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/ImageDrawing/Should_Render_DrawingBrushTransform.expected.png b/tests/TestFiles/Skia/Media/ImageDrawing/Should_Render_DrawingBrushTransform.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..35f1ca62e4e71945ec719639460ee29a8b2cf041 GIT binary patch literal 1188 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL9Be?5hW%z|fD}uylV=DA5Y%v_bO5>0o-U3d z6?5L+-k5tRLBuVvNup736C3l+a|UuvhDJ_NZVBy6x?QRnZKJKy8~?Aa`N}2F2Q+4| zg4-VrZ5!NAZ~Ehx->^37`c?C6=f2C#3^&vn85INt7=%WtksJh*E`MQo@OR%j`^&;{ zNps~6FpG0AbT%+d;J`)MUu>+O|NZ^M^8DwgPwcii&dTJVq`;uUL{qA0mK;OQ@wYWI t=N;(V;lSX*!qVV^gBq;a5a_NOd+pbL=JCFslBx-ErKhW(%Q~loCIDZJd{qDd literal 0 HcmV?d00001