Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rounded corner Border with animatable gradient properties? #10166

Open
mendedwraith opened this issue Nov 14, 2024 · 7 comments
Open

Rounded corner Border with animatable gradient properties? #10166

mendedwraith opened this issue Nov 14, 2024 · 7 comments
Labels
needs-triage Issue needs to be triaged by the area owners

Comments

@mendedwraith
Copy link

mendedwraith commented Nov 14, 2024

I've been stuck on trying to solve this for a while. It seems like a simple problem that has a simple solution, but I'm not sure it does yet.

I'm attempting to apply an animatable gradient to a Border or border-like composition.

The closest I was able to get was applying Microsoft.UI.Xaml.Media.RadialGradientBrush to Border.BorderBrush but it doesn't seem like the gradient properties or gradient-stop properties can be animated. It seems only the entire gradient can be transformed. Is this correct?

I can achieve what I want using a PNG file to represent the border and use composition to apply Microsoft.UI.Composition.CompositionRadialGradientBrush using a CompositionMaskBrush. However, this doesn't really seem scalable.

I was able to get close using a NineGridBrush with IsCenterHollow set to true but I don't see a way to round the inner corners.

Problems I've run into:

  1. Composition doesn't seem to allow "inner" geometry clipping.
  2. Microsoft.UI.Xaml.Media.RadialGradientBrush properties can't be animated?

Possible solutions:

  1. Something easy I'm missing?
  2. Using CompositionPathGeometry to create rounded border, feed into CreateSpriteShape and use CompositionRadialGradientBrush
  3. Layering visuals and adjusting CompositionMode
  4. Using RedirectVisual of an underlying Border element and somehow use a CompositionMaskBrush to apply the gradient.

Hope this isn't too much of an edge case 😉

@microsoft-github-policy-service microsoft-github-policy-service bot added the needs-triage Issue needs to be triaged by the area owners label Nov 14, 2024
@castorix
Copy link

Not sure if it is what you mean, but in this test I animate the gradient (CreateLinearGradientBrush, CreateColorGradientStop red to blue) from top to bottom with a translation matrix
(Matrix3x2.CreateTranslation by incrementing y, same method as in DirectWrite for first text in WinUI3_SwapChainPanel_DWriteCore)

Image

@mendedwraith
Copy link
Author

@castorix Yes that's essentially what I'm after. I'll have to do a bit of digging in.

I had a feeling I might have to mess with the visual layer, but was hoping composition, without need for external assets, could meet my needs. But I'll happily (hopefully) use SwapChainPanel and whatever APIs if that's the only way for now. As long as there are no visual and minimal latency issues.

Does anyone know:

  1. Can this be achieved with Visual layering? I tried many permutations of CompositionCompositeMode and I haven't found a way to composite a painted sprite onto only the painted portion of the sprite above or below, respecting transparency or not.
  2. Can Visual visual = ElementCompositionPreview.GetElementVisual(xaml_element) be converted to a VisualSprite ?
  3. Can something like this be done: CompositionSurfaceBrush RoundedBorderMaskBrush = _compositor.CreateSurfaceBrush( VisualToCompositionDrawingSurface(Visual) )

It's my guess that SwapChain will perform best on dynamic resizing whereas other solutions sacrifice animated gradient properties (Microsoft.UI.Xaml.Media.RadialGradientBrush) or will likely require polling on resize (most Microsoft.UI.Composition and canvas drawing techniques).

@castorix
Copy link

I did not use a SwapChain in this test, I used CreateRoundedRectangleGeometry, CreateSpriteShape, CreateShapeVisual.Add then SetElementChildVisual at end to set it as child of an image control.
I just used a timer (DispatcherTimer) to simulate the gradient animation with the matrix like with a SwapChain, but maybe can be done with Composition animations too...

@mendedwraith
Copy link
Author

mendedwraith commented Nov 14, 2024

@castorix Sorry for the misunderstanding. I've actually come to a similar solution but hit a wall when I wanted full transparency for everything but the border. The reason is that it's going to sit atop an acrylic background. Are you able to get transparency on the inner portion using that technique?

The Composition animations work for this. I assume Microsoft.UI.Xaml.Media.RadialGradientBrush has limitations due to platform independence but this project is solely Windows 11.

Edit: If not, maybe I could use CompositeEffect. I'll try it asap.

Edit 2: CompositeEffect involves a lot of beating around the bush. Perhaps drawing the border with CompositionPathGeometry and simply using Composition gradients and animations, with a fallback Border with BorderBrush set to Microsoft.UI.Xaml.Media.RadialGradientBrush for resizing is the ultimate answer.

@castorix
Copy link

This is the test I did inside an Image control :

 private Compositor _compositor;
 private ContainerVisual _container;
 int _nY = 0;
{
    _compositor = Microsoft.UI.Xaml.Hosting.ElementCompositionPreview.GetElementVisual(this.Content).Compositor;
    _container = _compositor.CreateContainerVisual();

    var roundedRectangleGeometry = _compositor.CreateRoundedRectangleGeometry();
    roundedRectangleGeometry.CornerRadius = new System.Numerics.Vector2(30);
    roundedRectangleGeometry.Size = new System.Numerics.Vector2((float)img1.Width, (float)img1.Height);

    var roundedRectangleShape = _compositor.CreateSpriteShape(roundedRectangleGeometry);

    // Linear gradient brush
    var gradientBrush = _compositor.CreateLinearGradientBrush();
    gradientBrush.StartPoint = new System.Numerics.Vector2(0, 0); // Top
    gradientBrush.EndPoint = new System.Numerics.Vector2(0, 1);   // Bottom
    gradientBrush.ExtendMode = CompositionGradientExtendMode.Mirror;
    
    var gradientStop1 = _compositor.CreateColorGradientStop();
    gradientStop1.Offset = 0.0f;
    gradientStop1.Color = Colors.Red;

    var gradientStop2 = _compositor.CreateColorGradientStop();
    gradientStop2.Offset = 1.0f;
    gradientStop2.Color = Colors.Blue;

    gradientBrush.ColorStops.Add(gradientStop1);
    gradientBrush.ColorStops.Add(gradientStop2);

    roundedRectangleShape.StrokeBrush = gradientBrush;
    roundedRectangleShape.StrokeThickness = 5;                
    roundedRectangleShape.FillBrush = null;
   
    var shapeVisual = _compositor.CreateShapeVisual();
    shapeVisual.Size = new System.Numerics.Vector2((float)img1.Width, (float)img1.Height);
    shapeVisual.Shapes.Add(roundedRectangleShape);
    
    ElementCompositionPreview.SetElementChildVisual(img1, shapeVisual);

    DispatcherTimer timer = new DispatcherTimer();
    timer.Interval = TimeSpan.FromMilliseconds(16); // ~60fps
    timer.Tick += (sender, e) =>
    {                   
        _nY += 10;                    
        var translationMatrix = System.Numerics.Matrix3x2.CreateTranslation(0, _nY);
        gradientBrush.TransformMatrix = translationMatrix;
        if (_nY >= img1.Height * 2)
            _nY = 0;
    };
    timer.Start();
}

XAML :

<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
    <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    <Image x:Name="img1" Width="300" Height="300" Source="Assets/Flowers.jpg" Stretch="UniformToFill"></Image>
</StackPanel>

Image

@mendedwraith
Copy link
Author

@castorix Oh wow, I crucially missed the option for StrokeThickness and StrokeBrush. The only thing that might help it look like a standard Border is an extra GeometryClip for the outside corners.

Well, I'm glad I used it as a bit of learning opportunity in case the other options are needed for custom geometry.

Thanks so much for your help.

@mendedwraith
Copy link
Author

mendedwraith commented Nov 15, 2024

Well, I was able to make a close approximation of what I'm looking for. I can't help but think that using GeometryPath will be more straightforward and yield better results. But this technique is definitely passable.

I have the gradient and animations set to values that make the desired effect obvious.

Screen.Recording.2024-11-15.143628.mp4
{
    _compositor = Microsoft.UI.Xaml.Hosting.ElementCompositionPreview.GetElementVisual(this.Content).Compositor;
    _container = _compositor.CreateContainerVisual();

    // UI container element
    Grid statusElement = FooterStatusHealthIndicator;

    // Sizing vars
    int strokeThickness = 15;
    int cornerRadius = 20;
    int outerCornerRadaius = cornerRadius - 2;

    // Rounded geometry for background/foreground
    CompositionRoundedRectangleGeometry roundedRectangleGeometry = _compositor.CreateRoundedRectangleGeometry();
    roundedRectangleGeometry.CornerRadius = new System.Numerics.Vector2(cornerRadius);
    roundedRectangleGeometry.Size = new System.Numerics.Vector2((float)statusElement.Width, (float)statusElement.Height);

    // Background
    CompositionSpriteShape bgRoundedRectangleShape = _compositor.CreateSpriteShape(roundedRectangleGeometry);

    // Foreground
    CompositionSpriteShape roundedRectangleShape = _compositor.CreateSpriteShape(roundedRectangleGeometry);

    // Create a Composition radial gradient
    CompositionRadialGradientBrush radialGradientBrush = _compositor.CreateRadialGradientBrush();
    radialGradientBrush.CenterPoint = new Vector2(110, 40);
    radialGradientBrush.InterpolationSpace = CompositionColorSpace.Rgb;
    radialGradientBrush.MappingMode = CompositionMappingMode.Absolute;
    radialGradientBrush.EllipseCenter = new Vector2(110, 40);
    radialGradientBrush.EllipseRadius = new Vector2(420, 180);
    radialGradientBrush.ExtendMode = CompositionGradientExtendMode.Mirror;
    radialGradientBrush.Scale = new Vector2(1.7f, 1.7f);

    // Create stops
    CompositionColorGradientStop highlightStop = _compositor.CreateColorGradientStop(0f, Color.FromArgb(225, 35, 210, 70));
    CompositionColorGradientStop fadeStop = _compositor.CreateColorGradientStop(.3f, Color.FromArgb(0, 35, 210, 70));

    // Create an animation for fadeStop
    ScalarKeyFrameAnimation fadeStopAnimation = _compositor.CreateScalarKeyFrameAnimation();
    fadeStopAnimation.InsertKeyFrame(0.0f, 0.3f);
    fadeStopAnimation.InsertKeyFrame(0.5f, 0.4f);
    fadeStopAnimation.InsertKeyFrame(1f, 0.3f);
    fadeStopAnimation.Duration = TimeSpan.FromSeconds(20);
    fadeStopAnimation.IterationBehavior = AnimationIterationBehavior.Forever;

    // Add fadeStop animation
    fadeStop.StartAnimation(nameof(fadeStop.Offset), fadeStopAnimation);

    // Add stops
    radialGradientBrush.ColorStops.Add(highlightStop);
    radialGradientBrush.ColorStops.Add(fadeStop);

    // Create animation for gradient rotation
    ScalarKeyFrameAnimation reverseRotationAnimation = _compositor.CreateScalarKeyFrameAnimation();
    reverseRotationAnimation.InsertKeyFrame(0f, 0f);           
    reverseRotationAnimation.InsertKeyFrame(1f, 360f);         
    reverseRotationAnimation.Duration = TimeSpan.FromSeconds(20);
    reverseRotationAnimation.IterationBehavior = AnimationIterationBehavior.Forever; // Repeat the animation

    // Start the animation on the RotationAngleInDegrees property
    radialGradientBrush.StartAnimation("RotationAngleInDegrees", reverseRotationAnimation);

    // Background border
    bgRoundedRectangleShape.StrokeBrush = _compositor.CreateColorBrush(Color.FromArgb(60, 12, 12, 12));
    bgRoundedRectangleShape.StrokeThickness = strokeThickness;
    bgRoundedRectangleShape.StrokeMiterLimit = 1f;
    bgRoundedRectangleShape.StrokeLineJoin = CompositionStrokeLineJoin.Round;
    bgRoundedRectangleShape.StrokeEndCap = CompositionStrokeCap.Round;
    bgRoundedRectangleShape.StrokeStartCap = CompositionStrokeCap.Round;
    bgRoundedRectangleShape.IsStrokeNonScaling = false;
    bgRoundedRectangleShape.FillBrush = null;

    // Foreground border
    roundedRectangleShape.StrokeBrush = radialGradientBrush;
    roundedRectangleShape.StrokeThickness = strokeThickness;
    roundedRectangleShape.StrokeMiterLimit = 1f;
    roundedRectangleShape.StrokeLineJoin = CompositionStrokeLineJoin.Round; 
    roundedRectangleShape.StrokeEndCap = CompositionStrokeCap.Round;
    roundedRectangleShape.StrokeStartCap = CompositionStrokeCap.Round;
    roundedRectangleShape.IsStrokeNonScaling = false;
    roundedRectangleShape.FillBrush = null;

    // Create ShapeVisual
    ShapeVisual shapeVisual = _compositor.CreateShapeVisual();
    shapeVisual.Size = new System.Numerics.Vector2((float)statusElement.Width, (float)statusElement.Height);

    // Add shapes to ShapeVIsual
    shapeVisual.Shapes.Add(bgRoundedRectangleShape);
    shapeVisual.Shapes.Add(roundedRectangleShape);

    CompositionRoundedRectangleGeometry roundedRectangleGeometryOuter = _compositor.CreateRoundedRectangleGeometry();
    roundedRectangleGeometryOuter.CornerRadius = new System.Numerics.Vector2(outerCornerRadaius);
    roundedRectangleGeometryOuter.Offset = new Vector2(0, 0.5f);
    roundedRectangleGeometryOuter.Size = new System.Numerics.Vector2((float)statusElement.Width + 0, (float)statusElement.Height - 0.5f);
            
    // Create clip
    CompositionGeometricClip roundedRectangleGeometryOuterClip = _compositor.CreateGeometricClip(roundedRectangleGeometryOuter);

    // Clip ShapeVisual
    shapeVisual.Clip = roundedRectangleGeometryOuterClip;
    shapeVisual.BorderMode = CompositionBorderMode.Soft;

    // Apply ShapeVisual to status element
    ElementCompositionPreview.SetElementChildVisual(statusElement, shapeVisual);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-triage Issue needs to be triaged by the area owners
Projects
None yet
Development

No branches or pull requests

2 participants