New Lights and PropertySet Interop – XAML and Visual Layer Interop, Part Two
In the last post, we explored using XamlCompositionBrushBase and LoadedImageSurface to create custom CompositionBrushes that can be used to paint XAML elements directly in your markup. In today’s post, we’ll continue our look into the new improvements made to the XAML and Visual Layer Interop APIs available in the Windows 10 Creators Update.
In this blog series, we’ll cover some of these improvements in the Creators Update and look at the following APIs:
- In Part 1:
- XamlCompositionBrushBase – easily paint a XAML UIElement with a CompositionBrush
- LoadedImageSurface – load an image easily and use with Composition APIs
- In Part 2, today’s post:
- XamlLights – apply lights to your XAML UI with a single line of XAML
- PointerPositionPropertySet – create 60 FPS animations using pointer position, off the UI thread!
- Enabling the Translation property – animate a XAML UI Element using Composition animation
If you’d like to review the previously available ElementCompositionPreview APIs, for example working with “hand-in” and “hand-out” Visuals, you can quickly catch up here.
Lighting UI with XamlLights
A powerful new feature in the Creators Update is the ability to set and use a Lighting effect directly in XAML by leveraging the abstract XamlLight class.
Creating a XamlLight starts just like a XamlCompositionBrushBase does, with an OnConnected and OnDisconnected method (see Part One here), but this time you inherit from the XamlLight subclass to create your own unique lighting that can be used directly in XAML. Microsoft uses this with the Reveal effect that comes with the Creators Update.
To see this in action, let’s build a demo that creates the animated GIF you see at the top of this post. It combines everything you learned about XamlCompositionBrushBase and LoadedImageSurface in the last post, but this example has two XamlLights: a HoverLight and an AmbientLight.
Let’s begin with creating the AmbientLight first. To get started, we begin similarly to the XamlCompositionBrushBase with an OnConnected and OnDisconnected method. However, for a XamlLight we set the CompositionLight property of the XamlLight subclass.
public class AmbLight : XamlLight { protected override void OnConnected(UIElement newElement) { Compositor compositor = Window.Current.Compositor; // Create AmbientLight and set its properties AmbientLight ambientLight = compositor.CreateAmbientLight(); ambientLight.Color = Colors.White; // Associate CompositionLight with XamlLight CompositionLight = ambientLight; // Add UIElement to the Light's Targets AmbLight.AddTargetElement(GetId(), newElement); } protected override void OnDisconnected(UIElement oldElement) { // Dispose Light when it is removed from the tree AmbLight.RemoveTargetElement(GetId(), oldElement); CompositionLight.Dispose(); } protected override string GetId() => typeof(AmbLight).FullName; }
With ambient lighting done, let’s build the SpotLight XamlLight. One of the main things we want the SpotLight to do is follow the user’s pointer. To do this, we can now use GetPointerPositionPropertySet method of ElementCompositionPreview to get a CompositionPropertySet we can use with a Composition ExpressionAnimation (PointerPositionPropertySet is explained in more detail in the PropertySets section below).
Here is the finished XamlLight implementation that creates that animated spotlight. Read the code comments to see the main parts of the effects, particularly how the resting position and animated offset position are used to create the lighting.
public class HoverLight : XamlLight { private ExpressionAnimation _lightPositionExpression; private Vector3KeyFrameAnimation _offsetAnimation; protected override void OnConnected(UIElement targetElement) { Compositor compositor = Window.Current.Compositor; // Create SpotLight and set its properties SpotLight spotLight = compositor.CreateSpotLight(); spotLight.InnerConeAngleInDegrees = 50f; spotLight.InnerConeColor = Colors.FloralWhite; spotLight.OuterConeAngleInDegrees = 0f; spotLight.ConstantAttenuation = 1f; spotLight.LinearAttenuation = 0.253f; spotLight.QuadraticAttenuation = 0.58f; // Associate CompositionLight with XamlLight this.CompositionLight = spotLight; // Define resting position Animation Vector3 restingPosition = new Vector3(200, 200, 400); CubicBezierEasingFunction cbEasing = compositor.CreateCubicBezierEasingFunction( new Vector2(0.3f, 0.7f), new Vector2(0.9f, 0.5f)); _offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); _offsetAnimation.InsertKeyFrame(1, restingPosition, cbEasing); _offsetAnimation.Duration = TimeSpan.FromSeconds(0.5f); spotLight.Offset = restingPosition; // Define expression animation that relates light's offset to pointer position CompositionPropertySet hoverPosition = ElementCompositionPreview.GetPointerPositionPropertySet(targetElement); _lightPositionExpression = compositor.CreateExpressionAnimation("Vector3(hover.Position.X, hover.Position.Y, height)"); _lightPositionExpression.SetReferenceParameter("hover", hoverPosition); _lightPositionExpression.SetScalarParameter("height", 15.0f); // Configure pointer entered/ exited events targetElement.PointerMoved += TargetElement_PointerMoved; targetElement.PointerExited += TargetElement_PointerExited; // Add UIElement to the Light's Targets HoverLight.AddTargetElement(GetId(), targetElement); } private void MoveToRestingPosition() { // Start animation on SpotLight's Offset CompositionLight?.StartAnimation("Offset", _offsetAnimation); } private void TargetElement_PointerMoved(object sender, PointerRoutedEventArgs e) { if (CompositionLight == null) return; // touch input is still UI thread-bound as of the Creators Update if (e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Touch) { Vector2 offset = e.GetCurrentPoint((UIElement)sender).Position.ToVector2(); (CompositionLight as SpotLight).Offset = new Vector3(offset.X, offset.Y, 15); } else { // Get the pointer's current position from the property and bind the SpotLight's X-Y Offset CompositionLight.StartAnimation("Offset", _lightPositionExpression); } } private void TargetElement_PointerExited(object sender, PointerRoutedEventArgs e) { // Move to resting state when pointer leaves targeted UIElement MoveToRestingPosition(); } protected override void OnDisconnected(UIElement oldElement) { // Dispose Light and Composition resources when it is removed from the tree HoverLight.RemoveTargetElement(GetId(), oldElement); CompositionLight.Dispose(); _lightPositionExpression.Dispose(); _offsetAnimation.Dispose(); } protected override string GetId() => typeof(HoverLight).FullName; }
Now, with the HoverLight class done, we can add both the AmbLight and the HoverLight to previous ImageEffectBrush (find ImageEffectBrush in the last post):
<Grid> <Grid.Background> <brushes:ImageEffectBrush ImageUriString="ms-appx:///Images/Background.png" /> </Grid.Background> <Grid.Lights> <lights:HoverLight/> <lights:AmbLight/> </Grid.Lights> </Grid>
Note: To add a XamlLight in markup, your Min SDK version must be set to Creators Update, otherwise you can set it in the code behind.
For more information, go here to read more about using XamlLight and here to see the Lighting documentation.
Using CompositionPropertySets
When you want to use the values of the ScrollViewer’s Offset or the Pointer’s X and Y position (e.g. mouse cursor) to do things like animate effects, you can use ElementCompositionPreview to retrieve their PropertySets. This allows you to create amazingly smooth, 60 FPS animations that are not tied to the UI thread. These methods let you get the values from user interaction for things like animations and lighting.
Using ScrollViewerManipulationPropertySet
This PropertySet is useful for animating things like Parallax, Translation and Opacity.
// Gets the manipulation <ScrollViewer x:Name="MyScrollViewer"/> CompositionPropertySet scrollViewerManipulationPropertySet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(MyScrollViewer);
To see an example, go to the Smooth Interaction and Motion blog post in this series. There is a section devoted to using the ScrollViewerManipulationPropertySet to drive the animation.
Using PointerPositionPropertySet (new!)
New in the Creators Update, is the PointerPositionPropertySet. This PropertySet is useful for creating animations for lighting and tilt. Like ScrollViewerManipulationPropertySet, PointerPositionPropertySet enables fast, smooth and UI thread independent animations.
A great example of this is the animation mechanism behind Fluent Design’s RevealBrush, where you see lighting effects on the edges on the UIElements. This effect is created by a CompositionLight, which has an Offset property animated by an ExpressionAnimation using the values obtained from the PointerPositionPropertySet.
// Useful for creating an ExpressionAnimation CompositionPropertySet pointerPositionPropertySet = ElementCompositionPreview.GetPointerPositionPropertySet(targetElement); ExpressionAnimation expressionAnimation = compositor.CreateExpressionAnimation("Vector3(param.Position.X, param.Position.Y, height)"); expressionAnimation.SetReferenceParameter("param", pointerPositionPropertySet);
To get a better understanding of how you can use this to power animations in your app, let’s explore XamlLights and create a demo that uses the PointerPositionPropertySet to animate a SpotLight.
Enabling Translation Property – Animating a XAML Element’s Offset using Composition Animations
As discussed in our previous blog post, property sharing between the Framework Layer and the Visual Layer used to be tricky prior to the Creators Update. The following Visual properties are shared between UIElements and their backing Visuals:
- Offset
- Scale
- Opacity
- TransformMatrix
- InsetClip
- CompositeMode
Prior to the Creators update, Scale and Offset were especially tricky because, as mentioned before, a UIElement isn’t aware of changes to the property values on the hand-out Visual, even though the hand-out Visual is aware of changes to the UIElement. Consequently, if you change the value of the hand-out Visual’s Offset or Size property and the UIElement’s position changes due to a page resize, the UIElement’s previous position values will stomp all over your hand-out Visual’s values.
Now with the Creators Update, this has become much easier to deal with as you can prevent Scale and Offset stomping by enabling the new Translation property on your element, by way of the ElementCompositionPreview object.
ElementCompositionPreview.SetIsTranslationEnabled(Rectangle1, true); //Now initialize the value of Translation in the PropertySet to zero for first use to avoid timing issues. This ensures that the property is ready for use immediately. var rect1VisualPropertySet = ElementCompositionPreview.GetElementVisual(Rectangle1).Properties; rect1VisualPropertySet.InsertVector3("Translation", Vector3.Zero);
Then, animate the visual’s Translation property where previously you would have animated its Offset property.
// Old way, subject to property stomping: visual.StartAnimation("Offset.Y", animation); // New way, available in the Creators Update visual.StartAnimation("Translation.Y", animation);
By animating a different property from the one affected during layout passes, you avoid any unwanted offset stomping coming from the XAML layer.
Wrapping up
In the past couple posts, we explored some of the new features of XAML and Composition Interop and how using Composition features in your XAML markup is easier than ever. From painting your UIElements with CompositionBrushes and applying lighting, to smooth off-UIThread animations, the power of the Composition API is more accessible than ever.
In the next post, we’ll dive deeper into how you can chain Composition effects to create amazing materials and help drive the evolution of Fluent Design.
Resources
- XAML Composition Interop Behavior Changes
- Visual Layer with XAML
- XamlCompositionBrushBase
- XamlLight
- Lighting Overview (Documentation)
- LoadedImageSurface
Source: New Lights and PropertySet Interop – XAML and Visual Layer Interop, Part Two
Leave a Reply