diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
index 2018763e733..9958828b7e1 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
+++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
@@ -565,6 +565,10 @@
TokenizingTextBoxPage.xaml
+
+
+ TransitionHelperPage.xaml
+
FullScreenModeStateTriggerPage.xaml
@@ -647,6 +651,9 @@
+
+
+
@@ -1111,6 +1118,10 @@
Designer
MSBuild:Compile
+
+ MSBuild:Compile
+ Designer
+
MSBuild:Compile
Designer
@@ -1500,7 +1511,11 @@
Visual C++ 2015 Runtime for Universal Windows Platform Apps
-
+
+
+ C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Numerics.dll
+
+
14.0
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Animations/Actions/StartTransitionActionXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Animations/Actions/StartTransitionActionXaml.bind
new file mode 100644
index 00000000000..a555f9bd880
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Animations/Actions/StartTransitionActionXaml.bind
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Windows Community Toolkit
+
+
+
+
+
+
+
+
+
+ Windows Community Toolkit
+
+
+ The Windows Community Toolkit is a collection of helper functions, custom controls, and app services. It simplifies and demonstrates common developer patterns when building experiences for Windows.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/CustomTextScalingCalculator.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/CustomTextScalingCalculator.cs
new file mode 100644
index 00000000000..60d23c46da2
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/CustomTextScalingCalculator.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using Microsoft.Toolkit.Uwp.UI;
+using Microsoft.Toolkit.Uwp.UI.Animations.Helpers;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ public sealed class CustomTextScalingCalculator : IScalingCalculator
+ {
+ ///
+ public Vector2 GetScaling(UIElement source, UIElement target)
+ {
+ var sourceTextElement = source?.FindDescendantOrSelf();
+ var targetTextElement = target?.FindDescendantOrSelf();
+ if (sourceTextElement is not null && targetTextElement is not null)
+ {
+ var scale = targetTextElement.FontSize / sourceTextElement.FontSize;
+ return new Vector2((float)scale);
+ }
+
+ return new Vector2(1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperCode.bind
new file mode 100644
index 00000000000..58595af852b
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperCode.bind
@@ -0,0 +1,35 @@
+// Create a TransitionHelper.
+var transitionHelper = new TransitionHelper();
+
+// Configure the elements that need to be connected by animation, if the elements use the default configuration, they do not need to be listed here.
+transitionHelper.Configs = new List
+ {
+ new()
+ {
+ Id = "background",
+ ScaleMode = ScaleMode.Scale
+ },
+ new()
+ {
+ Id = "image",
+ ScaleMode = ScaleMode.Scale
+ },
+ new()
+ {
+ Id = "name",
+ ScaleMode = ScaleMode.ScaleY
+ },
+ new()
+ {
+ Id = "desc",
+ EnableClipAnimation = true
+ },
+ };
+transitionHelper.Source = FirstControl;
+transitionHelper.Target = SecondControl;
+
+// Animate.
+await transitionHelper.AnimateAsync();
+
+// Reverse.
+await transitionHelper.ReverseAsync();
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperPage.xaml
new file mode 100644
index 00000000000..980990f5998
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperPage.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperPage.xaml.cs
new file mode 100644
index 00000000000..d45a16a20b0
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperPage.xaml.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI.Xaml;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ public sealed partial class TransitionHelperPage : IXamlRenderListener
+ {
+ public TransitionHelperPage()
+ {
+ InitializeComponent();
+ }
+
+ public void OnXamlRendered(FrameworkElement control)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperXaml.bind
new file mode 100644
index 00000000000..5ed8a59087e
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TransitionHelper/TransitionHelperXaml.bind
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Magic
+ Magic is a cute 😺.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Magic is my cat's name
+
+
+ Magic is a cute 😺, but sometimes very naughty.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
index 63c7795dbdf..8971c60f2fd 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
@@ -671,6 +671,26 @@
"XamlCodeFile": "/SamplePages/Animations/Shadows/AnimatedCardShadowXaml.bind",
"Icon": "/SamplePages/Shadows/DropShadowPanel.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/helpers/AttachedShadows.md"
+ },
+ {
+ "Name": "TransitionHelper",
+ "Type": "TransitionHelperPage",
+ "Subcategory": "Helpers",
+ "About": "A animation helper that morphs between two controls.",
+ "CodeUrl": "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.UI.Controls.Animations/Helpers",
+ "XamlCodeFile": "/SamplePages/TransitionHelper/TransitionHelperXaml.bind",
+ "CodeFile": "/SamplePages/TransitionHelper/TransitionHelperCode.bind",
+ "Icon": "/Assets/Helpers.png",
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/animations/TransitionHelper.md"
+ },
+ {
+ "Name": "StartTransitionAction",
+ "Subcategory": "Actions",
+ "About": "An action that can trigger the target TransitionHelper instance.",
+ "CodeUrl": "https://github.com/CommunityToolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.UI.Behaviors/Animations/StartTransitionAction.cs",
+ "XamlCodeFile": "/SamplePages/Animations/Actions/StartTransitionActionXaml.bind",
+ "Icon": "/Assets/Helpers.png",
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/animations/TransitionHelper.md"
}
]
},
diff --git a/Microsoft.Toolkit.Uwp.UI.Animations/Enums/ScaleMode.cs b/Microsoft.Toolkit.Uwp.UI.Animations/Enums/ScaleMode.cs
new file mode 100644
index 00000000000..b125652b18f
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Animations/Enums/ScaleMode.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+namespace Microsoft.Toolkit.Uwp.UI.Animations
+{
+ ///
+ /// Indicates the strategy when the scale property of a UI element is animated.
+ ///
+ public enum ScaleMode
+ {
+ ///
+ /// Do not make any changes to the scale attribute of the UI element.
+ ///
+ None,
+
+ ///
+ /// Apply the scaling changes to the horizontal and vertical directions of the UI element.
+ ///
+ Scale,
+
+ ///
+ /// Apply the scaling changes to the horizontal and vertical directions of the UI element,
+ /// but the value is calculated based on the change in the horizontal direction.
+ ///
+ ScaleX,
+
+ ///
+ /// Apply scaling changes to the horizontal and vertical directions of the UI element,
+ /// but the value is calculated based on the change in the vertical direction.
+ ///
+ ScaleY,
+
+ ///
+ /// Apply the scaling changes calculated by using custom scaling calculator.
+ ///
+ Custom,
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Animations/Enums/VisualStateToggleMethod.cs b/Microsoft.Toolkit.Uwp.UI.Animations/Enums/VisualStateToggleMethod.cs
new file mode 100644
index 00000000000..686894d72c1
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Animations/Enums/VisualStateToggleMethod.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+namespace Microsoft.Toolkit.Uwp.UI.Animations
+{
+ ///
+ /// Indicates the method of changing the visibility of UI elements.
+ ///
+ public enum VisualStateToggleMethod
+ {
+ ///
+ /// Change the visibility of UI elements by modifying the Visibility property.
+ ///
+ ByVisibility,
+
+ ///
+ /// Change the visibility of UI elements by modifying the IsVisible property of it's Visual.
+ ///
+ ByIsVisible
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/IScalingCalculator.cs b/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/IScalingCalculator.cs
new file mode 100644
index 00000000000..9a822d06581
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/IScalingCalculator.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System.Numerics;
+using Windows.UI.Xaml;
+
+namespace Microsoft.Toolkit.Uwp.UI.Animations.Helpers
+{
+ ///
+ /// Defines methods to support calculating scaling changes.
+ ///
+ public interface IScalingCalculator
+ {
+ ///
+ /// Gets the scaling changes when the source element transitions to the target element.
+ ///
+ /// The source element.
+ /// The target element.
+ /// A whose X value represents the horizontal scaling change and whose Y represents the vertical scaling change.
+ Vector2 GetScaling(UIElement source, UIElement target);
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/TransitionConfig.cs b/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/TransitionConfig.cs
new file mode 100644
index 00000000000..577decb6dac
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/TransitionConfig.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using Microsoft.Toolkit.Uwp.UI.Animations.Helpers;
+using Windows.Foundation;
+using Windows.UI.Xaml.Media.Animation;
+
+namespace Microsoft.Toolkit.Uwp.UI.Animations
+{
+ ///
+ /// Configuration used for the transition between UI elements.
+ ///
+ public class TransitionConfig
+ {
+ ///
+ /// Gets or sets an id to indicate the target UI elements.
+ ///
+ public string? Id { get; set; }
+
+ ///
+ /// Gets or sets the scale strategy of the transition.
+ /// The default value is .
+ ///
+ public ScaleMode ScaleMode { get; set; } = ScaleMode.None;
+
+ ///
+ /// Gets or sets the custom scale calculator.
+ /// Only works when is .
+ /// If this value is not set, the scale strategy will fall back to .
+ ///
+ public IScalingCalculator? CustomScalingCalculator { get; set; } = null;
+
+ ///
+ /// Gets or sets a value indicating whether clip animations are enabled for the target UI elements.
+ ///
+ public bool EnableClipAnimation { get; set; }
+
+ ///
+ /// Gets or sets the center point used to calculate the element's translation or scale when animating.
+ /// Value is normalized with respect to the size of the animated element.
+ /// For example, a value of (0.0, 0.5) means that this point is at the leftmost point of the element horizontally and the center of the element vertically.
+ /// The default value is (0, 0).
+ ///
+ public Point NormalizedCenterPoint { get; set; } = default;
+
+ ///
+ /// Gets or sets the easing function type for the transition.
+ /// If this value is not set, it will fall back to the value in .
+ ///
+ public EasingType? EasingType { get; set; } = null;
+
+ ///
+ /// Gets or sets the easing function mode for the transition.
+ /// If this value is not set, it will fall back to the value in .
+ ///
+ public EasingMode? EasingMode { get; set; } = null;
+
+ ///
+ /// Gets or sets the key point of opacity transition.
+ /// The time the keyframe of opacity from 0 to 1 or from 1 to 0 should occur at, expressed as a percentage of the animation duration. The allowed values are from (0, 0) to (1, 1).
+ /// .X will be used in the animation of the normal direction.
+ /// .Y will be used in the animation of the reverse direction.
+ /// If this value is not set, it will fall back to the value in .
+ ///
+ public Point? OpacityTransitionProgressKey { get; set; } = null;
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/TransitionHelper.Animation.cs b/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/TransitionHelper.Animation.cs
new file mode 100644
index 00000000000..ebc7b347f90
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Animations/Helpers/TransitionHelper.Animation.cs
@@ -0,0 +1,442 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.UI.Composition;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Hosting;
+using Windows.UI.Xaml.Media.Animation;
+
+namespace Microsoft.Toolkit.Uwp.UI.Animations
+{
+ ///
+ /// A animation helper that morphs between two controls.
+ ///
+ public sealed partial class TransitionHelper
+ {
+ private const string TranslationPropertyName = "Translation";
+ private const string TranslationXYPropertyName = "Translation.XY";
+ private const string ScaleXYPropertyName = "Scale.XY";
+
+ private interface IEasingFunctionFactory
+ {
+ CompositionEasingFunction? GetEasingFunction(Compositor compositor, bool inverse);
+ }
+
+ private interface IKeyFrameCompositionAnimationFactory
+ {
+ KeyFrameAnimation GetAnimation(CompositionObject targetHint, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, out CompositionObject? target);
+ }
+
+ private interface IKeyFrameAnimationGroupController
+ {
+ float? LastStopProgress { get; }
+
+ AnimationDirection? CurrentDirection { get; }
+
+ Task StartAsync(CancellationToken token, TimeSpan? duration);
+
+ Task ReverseAsync(CancellationToken token, bool inverseEasingFunction, TimeSpan? duration);
+
+ void AddAnimationFor(UIElement target, IKeyFrameCompositionAnimationFactory? factory);
+
+ void AddAnimationGroupFor(UIElement target, IKeyFrameCompositionAnimationFactory?[] factories);
+ }
+
+ private sealed record EasingFunctionFactory(
+ EasingType Type = EasingType.Default,
+ EasingMode Mode = EasingMode.EaseInOut,
+ bool Inverse = false)
+ : IEasingFunctionFactory
+ {
+ public CompositionEasingFunction? GetEasingFunction(Compositor compositor, bool inverse)
+ {
+ if (Type == EasingType.Linear)
+ {
+ return compositor.CreateLinearEasingFunction();
+ }
+
+ var inversed = Inverse ^ inverse;
+ if (Type == EasingType.Default && Mode == EasingMode.EaseInOut)
+ {
+ return inversed ? compositor.CreateCubicBezierEasingFunction(new(1f, 0.06f), new(0.59f, 0.48f)) : null;
+ }
+
+ var (a, b) = AnimationExtensions.EasingMaps[(Type, Mode)];
+ return inversed ? compositor.CreateCubicBezierEasingFunction(new(1 - b.X, 1 - b.Y), new(1 - a.X, 1 - a.Y)) : compositor.CreateCubicBezierEasingFunction(a, b);
+ }
+ }
+
+ private sealed record KeyFrameAnimationFactory(
+ string Property,
+ T To,
+ T? From,
+ TimeSpan? Delay,
+ TimeSpan? Duration,
+ IEasingFunctionFactory? EasingFunctionFactory,
+ Dictionary? NormalizedKeyFrames,
+ Dictionary? ReversedNormalizedKeyFrames)
+ : IKeyFrameCompositionAnimationFactory
+ where T : unmanaged
+ {
+ public KeyFrameAnimation GetAnimation(CompositionObject targetHint, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, out CompositionObject? target)
+ {
+ target = null;
+
+ var direction = reversed ? AnimationDirection.Reverse : AnimationDirection.Normal;
+ var keyFrames = (useReversedKeyframes && ReversedNormalizedKeyFrames is not null) ? ReversedNormalizedKeyFrames : NormalizedKeyFrames;
+
+ if (typeof(T) == typeof(float))
+ {
+ var scalarAnimation = targetHint.Compositor.CreateScalarKeyFrameAnimation(
+ Property,
+ CastTo(To),
+ CastToNullable(From),
+ Delay,
+ Duration,
+ EasingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction),
+ direction: direction);
+ if (keyFrames?.Count > 0)
+ {
+ foreach (var item in keyFrames)
+ {
+ var (value, easingFunctionFactory) = item.Value;
+ scalarAnimation.InsertKeyFrame(item.Key, CastTo(value), easingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction));
+ }
+ }
+
+ return scalarAnimation;
+ }
+
+ if (typeof(T) == typeof(Vector2))
+ {
+ var vector2Animation = targetHint.Compositor.CreateVector2KeyFrameAnimation(
+ Property,
+ CastTo(To),
+ CastToNullable(From),
+ Delay,
+ Duration,
+ EasingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction),
+ direction: direction);
+ if (keyFrames?.Count > 0)
+ {
+ foreach (var item in keyFrames)
+ {
+ var (value, easingFunctionFactory) = item.Value;
+ vector2Animation.InsertKeyFrame(item.Key, CastTo(value), easingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction));
+ }
+ }
+
+ return vector2Animation;
+ }
+
+ throw new InvalidOperationException("Invalid animation type");
+ }
+
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private TValue CastTo(T value)
+ where TValue : unmanaged
+ {
+ return (TValue)(object)value;
+ }
+
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private TValue? CastToNullable(T? value)
+ where TValue : unmanaged
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ T validValue = value.GetValueOrDefault();
+
+ return (TValue)(object)validValue;
+ }
+ }
+
+ private sealed record ClipScalarAnimationFactory(
+ string Property,
+ float To,
+ float? From,
+ TimeSpan? Delay,
+ TimeSpan? Duration,
+ IEasingFunctionFactory? EasingFunctionFactory)
+ : IKeyFrameCompositionAnimationFactory
+ {
+ public KeyFrameAnimation GetAnimation(CompositionObject targetHint, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, out CompositionObject target)
+ {
+ var direction = reversed ? AnimationDirection.Reverse : AnimationDirection.Normal;
+ var visual = (Visual)targetHint;
+ var clip = visual.Clip as InsetClip ?? (InsetClip)(visual.Clip = visual.Compositor.CreateInsetClip());
+ var easingFunction = EasingFunctionFactory?.GetEasingFunction(clip.Compositor, inverseEasingFunction);
+ var animation = clip.Compositor.CreateScalarKeyFrameAnimation(
+ Property,
+ To,
+ From,
+ Delay,
+ Duration,
+ easingFunction,
+ direction: direction);
+
+ target = clip;
+ return animation;
+ }
+ }
+
+ private IKeyFrameCompositionAnimationFactory[] Clip(
+ Thickness to,
+ IEasingFunctionFactory? easingFunctionFactory,
+ Thickness? from = null,
+ TimeSpan? delay = null,
+ TimeSpan? duration = null)
+ {
+ return new[]
+ {
+ new ClipScalarAnimationFactory(
+ nameof(InsetClip.LeftInset),
+ (float)to.Left,
+ (float?)from?.Left,
+ delay,
+ duration,
+ easingFunctionFactory),
+ new ClipScalarAnimationFactory(
+ nameof(InsetClip.TopInset),
+ (float)to.Top,
+ (float?)from?.Top,
+ delay,
+ duration,
+ easingFunctionFactory),
+ new ClipScalarAnimationFactory(
+ nameof(InsetClip.RightInset),
+ (float)to.Right,
+ (float?)from?.Right,
+ delay,
+ duration,
+ easingFunctionFactory),
+ new ClipScalarAnimationFactory(
+ nameof(InsetClip.BottomInset),
+ (float)to.Bottom,
+ (float?)from?.Bottom,
+ delay,
+ duration,
+ easingFunctionFactory)
+ };
+ }
+
+ private IKeyFrameCompositionAnimationFactory Translation(
+ Vector2 to,
+ IEasingFunctionFactory? easingFunctionFactory,
+ Vector2? from = null,
+ TimeSpan? delay = null,
+ TimeSpan? duration = null,
+ Dictionary? normalizedKeyFrames = null,
+ Dictionary? reversedNormalizedKeyFrames = null)
+ {
+ return new KeyFrameAnimationFactory(TranslationXYPropertyName, to, from, delay, duration, easingFunctionFactory, normalizedKeyFrames, reversedNormalizedKeyFrames);
+ }
+
+ private IKeyFrameCompositionAnimationFactory Opacity(
+ double to,
+ IEasingFunctionFactory? easingFunctionFactory,
+ double? from = null,
+ TimeSpan? delay = null,
+ TimeSpan? duration = null,
+ Dictionary? normalizedKeyFrames = null,
+ Dictionary? reversedNormalizedKeyFrames = null)
+ {
+ return new KeyFrameAnimationFactory(nameof(Visual.Opacity), (float)to, (float?)from, delay, duration, easingFunctionFactory, normalizedKeyFrames, reversedNormalizedKeyFrames);
+ }
+
+ private IKeyFrameCompositionAnimationFactory Scale(
+ Vector2 to,
+ IEasingFunctionFactory? easingFunctionFactory,
+ Vector2? from = null,
+ TimeSpan? delay = null,
+ TimeSpan? duration = null,
+ Dictionary? normalizedKeyFrames = null,
+ Dictionary? reversedNormalizedKeyFrames = null)
+ {
+ return new KeyFrameAnimationFactory(ScaleXYPropertyName, to, from, delay, duration, easingFunctionFactory, normalizedKeyFrames, reversedNormalizedKeyFrames);
+ }
+
+ private sealed class KeyFrameAnimationGroupController : IKeyFrameAnimationGroupController
+ {
+ private readonly Dictionary> animationFactories = new();
+
+ public float? LastStopProgress { get; private set; } = null;
+
+ public AnimationDirection? CurrentDirection { get; private set; } = null;
+
+ private bool _lastInverseEasingFunction = false;
+
+ private bool _lastStartInNormalDirection = true;
+
+ public void AddAnimationFor(UIElement target, IKeyFrameCompositionAnimationFactory? factory)
+ {
+ if (factory is null)
+ {
+ return;
+ }
+
+ if (animationFactories.ContainsKey(target))
+ {
+ animationFactories[target].Add(factory);
+ }
+ else
+ {
+ animationFactories.Add(target, new List() { factory });
+ }
+ }
+
+ public void AddAnimationGroupFor(UIElement target, IKeyFrameCompositionAnimationFactory?[] factories)
+ {
+ var validFactories = factories.Where(factory => factory is not null);
+ if (validFactories.Any() is false)
+ {
+ return;
+ }
+
+ if (animationFactories.ContainsKey(target))
+ {
+ animationFactories[target].AddRange(validFactories!);
+ }
+ else
+ {
+ animationFactories.Add(target, new List(validFactories!));
+ }
+ }
+
+ public Task StartAsync(CancellationToken token, TimeSpan? duration)
+ {
+ var start = this.LastStopProgress;
+ var isInterruptedAnimation = start.HasValue;
+ if (isInterruptedAnimation is false)
+ {
+ this._lastStartInNormalDirection = true;
+ }
+
+ var inverseEasing = isInterruptedAnimation && this._lastInverseEasingFunction;
+ var useReversedKeyframes = isInterruptedAnimation && !this._lastStartInNormalDirection;
+ return AnimateAsync(false, useReversedKeyframes, token, inverseEasing, duration, start);
+ }
+
+ public Task ReverseAsync(CancellationToken token, bool inverseEasingFunction, TimeSpan? duration)
+ {
+ float? start = null;
+ if (this.LastStopProgress.HasValue)
+ {
+ start = 1 - this.LastStopProgress.Value;
+ }
+
+ var isInterruptedAnimation = start.HasValue;
+ if (isInterruptedAnimation is false)
+ {
+ this._lastStartInNormalDirection = false;
+ }
+
+ var inverseEasing = (isInterruptedAnimation && this._lastInverseEasingFunction) || (!isInterruptedAnimation && inverseEasingFunction);
+ var useReversedKeyframes = !isInterruptedAnimation || !this._lastStartInNormalDirection;
+ return AnimateAsync(true, useReversedKeyframes, token, inverseEasing, duration, start);
+ }
+
+ private Task AnimateAsync(bool reversed, bool useReversedKeyframes, CancellationToken token, bool inverseEasingFunction, TimeSpan? duration, float? startProgress)
+ {
+ List? tasks = null;
+ List<(CompositionObject Target, string Path)>? compositionAnimations = null;
+ DateTime? animationStartTime = null;
+ this.LastStopProgress = null;
+ this.CurrentDirection = reversed ? AnimationDirection.Reverse : AnimationDirection.Normal;
+ this._lastInverseEasingFunction = inverseEasingFunction;
+ if (this.animationFactories.Count > 0)
+ {
+ if (duration.HasValue)
+ {
+ var elapsedDuration = duration.Value * (startProgress ?? 0d);
+ animationStartTime = DateTime.Now - elapsedDuration;
+ }
+
+ tasks = new List(this.animationFactories.Count);
+ compositionAnimations = new List<(CompositionObject Target, string Path)>();
+ foreach (var item in this.animationFactories)
+ {
+ tasks.Add(StartForAsync(item.Key, reversed, useReversedKeyframes, inverseEasingFunction, duration, startProgress, compositionAnimations));
+ }
+ }
+
+ static void Stop(object state)
+ {
+ var (controller, reversed, duration, animationStartTime, animations) = ((KeyFrameAnimationGroupController, bool, TimeSpan?, DateTime?, List<(CompositionObject Target, string Path)>))state;
+ foreach (var (target, path) in animations)
+ {
+ target.StopAnimation(path);
+ }
+
+ if (duration.HasValue is false || animationStartTime.HasValue is false)
+ {
+ return;
+ }
+
+ var stopProgress = Math.Max(0, Math.Min((DateTime.Now - animationStartTime.Value) / duration.Value, 1));
+ controller.LastStopProgress = (float)(reversed ? 1 - stopProgress : stopProgress);
+ }
+
+ if (compositionAnimations is not null)
+ {
+ token.Register(static obj => Stop(obj), (this, reversed, duration, animationStartTime, compositionAnimations));
+ }
+
+ return tasks is null ? Task.CompletedTask : Task.WhenAll(tasks);
+ }
+
+ private Task StartForAsync(UIElement element, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, TimeSpan? duration, float? startProgress, List<(CompositionObject Target, string Path)> animations)
+ {
+ if (!this.animationFactories.TryGetValue(element, out var factories) || factories.Count > 0 is false)
+ {
+ return Task.CompletedTask;
+ }
+
+ ElementCompositionPreview.SetIsTranslationEnabled(element, true);
+ var visual = element.GetVisual();
+ var batch = visual.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
+ var taskCompletionSource = new TaskCompletionSource