From a65b8b36784b521cd2b74f54f0fcb0476d44e1c4 Mon Sep 17 00:00:00 2001
From: michael-hawker <24302614+michael-hawker@users.noreply.github.com>
Date: Thu, 15 Dec 2022 16:28:27 -0800
Subject: [PATCH] Initial port of code from TransitionHelper PR
https://github.com/CommunityToolkit/WindowsCommunityToolkit/pull/4338
(Still not compiling)
---
Windows.Toolkit.Common.props | 2 +-
.../CustomTextScalingCalculator.cs | 26 +
.../TransitionHelper.Samples.csproj | 24 +
.../TransitionHelper.md | 51 +-
.../TransitionHelperCustomSample.xaml | 21 -
.../TransitionHelperCustomSample.xaml.cs | 27 -
.../TransitionHelperFullExample.xaml | 241 ++++++++
.../TransitionHelperFullExample.xaml.cs | 16 +
.../TransitionHelperTemplatedSample.xaml | 16 -
.../TransitionHelperTemplatedSample.xaml.cs | 24 -
...itionHelperTemplatedStyleCustomSample.xaml | 26 -
...onHelperTemplatedStyleCustomSample.xaml.cs | 24 -
.../TransitionHelperXbindBackedSample.xaml | 16 -
.../TransitionHelperXbindBackedSample.xaml.cs | 24 -
...ionHelperXbindBackedStyleCustomSample.xaml | 26 -
...HelperXbindBackedStyleCustomSample.xaml.cs | 24 -
.../src/Behaviors/ReverseTransitionAction.cs | 57 ++
.../src/Behaviors/StartTransitionAction.cs | 119 ++++
labs/TransitionHelper/src/Dependencies.props | 10 +-
labs/TransitionHelper/src/Enums/ScaleMode.cs | 38 ++
.../src/Enums/VisualStateToggleMethod.cs | 21 +
.../src/IScalingCalculator.cs | 21 +
.../src/Properties/IsExternalInit.cs | 16 +
labs/TransitionHelper/src/Themes/Generic.xaml | 10 -
.../src/Toolkit/AnimationExtensions.cs | 70 +++
labs/TransitionHelper/src/TransitionConfig.cs | 71 +++
.../src/TransitionHelper.Animation.cs | 446 ++++++++++++++
.../src/TransitionHelper.AttachedProperty.cs | 105 ++++
.../src/TransitionHelper.Helpers.cs | 276 +++++++++
.../src/TransitionHelper.Logic.cs | 574 ++++++++++++++++++
.../src/TransitionHelper.Properties.cs | 204 +++++++
labs/TransitionHelper/src/TransitionHelper.cs | 174 +++---
.../TransitionHelperStyle_ClassicBinding.xaml | 62 --
.../src/TransitionHelperStyle_xBind.xaml | 69 ---
.../src/TransitionHelperStyle_xBind.xaml.cs | 20 -
.../src/TransitionHelper_ClassicBinding.cs | 94 ---
.../src/TransitionHelper_xBind.cs | 71 ---
37 files changed, 2434 insertions(+), 682 deletions(-)
create mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/CustomTextScalingCalculator.cs
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml.cs
create mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml
create mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml.cs
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml.cs
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml.cs
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml.cs
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml
delete mode 100644 labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml.cs
create mode 100644 labs/TransitionHelper/src/Behaviors/ReverseTransitionAction.cs
create mode 100644 labs/TransitionHelper/src/Behaviors/StartTransitionAction.cs
create mode 100644 labs/TransitionHelper/src/Enums/ScaleMode.cs
create mode 100644 labs/TransitionHelper/src/Enums/VisualStateToggleMethod.cs
create mode 100644 labs/TransitionHelper/src/IScalingCalculator.cs
create mode 100644 labs/TransitionHelper/src/Properties/IsExternalInit.cs
delete mode 100644 labs/TransitionHelper/src/Themes/Generic.xaml
create mode 100644 labs/TransitionHelper/src/Toolkit/AnimationExtensions.cs
create mode 100644 labs/TransitionHelper/src/TransitionConfig.cs
create mode 100644 labs/TransitionHelper/src/TransitionHelper.Animation.cs
create mode 100644 labs/TransitionHelper/src/TransitionHelper.AttachedProperty.cs
create mode 100644 labs/TransitionHelper/src/TransitionHelper.Helpers.cs
create mode 100644 labs/TransitionHelper/src/TransitionHelper.Logic.cs
create mode 100644 labs/TransitionHelper/src/TransitionHelper.Properties.cs
delete mode 100644 labs/TransitionHelper/src/TransitionHelperStyle_ClassicBinding.xaml
delete mode 100644 labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml
delete mode 100644 labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml.cs
delete mode 100644 labs/TransitionHelper/src/TransitionHelper_ClassicBinding.cs
delete mode 100644 labs/TransitionHelper/src/TransitionHelper_xBind.cs
diff --git a/Windows.Toolkit.Common.props b/Windows.Toolkit.Common.props
index 51218d281..e97e92774 100644
--- a/Windows.Toolkit.Common.props
+++ b/Windows.Toolkit.Common.props
@@ -9,7 +9,7 @@
(c) .NET Foundation and Contributors. All rights reserved.https://github.com/CommunityToolkit/Labs-Windowshttps://github.com/CommunityToolkit/Labs-Windows/releases
- Icon.png
+
https://raw.githubusercontent.com/CommunityToolkit/Labs-Windows/main/nuget.png$(NoWarn);NU1505;NU1504
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/CustomTextScalingCalculator.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/CustomTextScalingCalculator.cs
new file mode 100644
index 000000000..a671becb7
--- /dev/null
+++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/CustomTextScalingCalculator.cs
@@ -0,0 +1,26 @@
+// 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;
+
+namespace TransitionHelperExperiment.Samples;
+
+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);
+ }
+}
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj
index 10b2ca7b5..bae97a860 100644
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj
+++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj
@@ -14,10 +14,34 @@
TransitionHelperExperiment.SamplesTransitionHelperExperiment.Samples
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md
index 485b361c9..d4d32a83e 100644
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md
+++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md
@@ -1,7 +1,7 @@
---
title: TransitionHelper
author: githubaccount
-description: TODO: Your experiment's description here
+description: An animation helper that morphs between two controls.
keywords: TransitionHelper, Control, Layout
dev_langs:
- csharp
@@ -11,54 +11,11 @@ discussion-id: 0
issue-id: 0
---
-
-
-
-
-
-
-
-
# TransitionHelper
-TODO: Fill in information about this experiment and how to get started here...
-
-## Custom Control
-
-You can inherit from an existing component as well, like `Panel`, this example shows a control without a
-XAML Style that will be more light-weight to consume by an app developer:
-
-> [!Sample TransitionHelperCustomSample]
-
-## Templated Controls
-
-The Toolkit is built with templated controls. This provides developers a flexible way to restyle components
-easily while still inheriting the general functionality a control provides. The examples below show
-how a component can use a default style and then get overridden by the end developer.
-
-TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component.
-Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind`
-classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name.
-
-The `_ClassicBinding` class shows the traditional method used to develop components with best practices.
-
-### Implict style
-
-> [!SAMPLE TransitionHelperTemplatedSample]
-
-### Custom style
-
-> [!SAMPLE TransitionHelperTemplatedStyleCustomSample]
-
-## Templated Controls with x:Bind
-
-This is an _experimental_ new way to define components which allows for the use of x:Bind within the style.
-
-### Implict style
-
-> [!SAMPLE TransitionHelperXbindBackedSample]
+An animation helper that morphs between two controls.
-### Custom style
+### Example
-> [!SAMPLE TransitionHelperXbindBackedStyleCustomSample]
+> [!SAMPLE TransitionHelperFullExample]
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml
deleted file mode 100644
index a237bd8fe..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml.cs
deleted file mode 100644
index 01740f754..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// 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.
-
-namespace TransitionHelperExperiment.Samples;
-
-///
-/// An example sample page of a custom control inheriting from Panel.
-///
-[ToolkitSampleMultiChoiceOption("LayoutOrientation", title: "Orientation", "Horizontal", "Vertical")]
-
-[ToolkitSample(id: nameof(TransitionHelperCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(TransitionHelper)} custom control.")]
-public sealed partial class TransitionHelperCustomSample : Page
-{
- public TransitionHelperCustomSample()
- {
- this.InitializeComponent();
- }
-
- // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149
- public static Orientation ConvertStringToOrientation(string orientation) => orientation switch
- {
- "Vertical" => Orientation.Vertical,
- "Horizontal" => Orientation.Horizontal,
- _ => throw new System.NotImplementedException(),
- };
-}
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml
new file mode 100644
index 000000000..be0b3bd74
--- /dev/null
+++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Magic
+ Magic is a cute 😺.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Magic is my cat's name
+
+
+ Magic is a cute 😺, but sometimes very naughty.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml.cs
new file mode 100644
index 000000000..ae580a901
--- /dev/null
+++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml.cs
@@ -0,0 +1,16 @@
+// 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.
+
+namespace TransitionHelperExperiment.Samples;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class TransitionHelperFullExample : Page
+{
+ public TransitionHelperFullExample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml
deleted file mode 100644
index 00cf16c13..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml.cs
deleted file mode 100644
index a917a6239..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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.
-
-namespace TransitionHelperExperiment.Samples;
-
-[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)]
-// Single values without a colon are used for both label and value.
-// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value").
-[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")]
-[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")]
-[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground",
- "Teal : #0ddc8c",
- "Sand : #e7a676",
- "Dull green : #5d7577")]
-
-[ToolkitSample(id: nameof(TransitionHelperTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")]
-public sealed partial class TransitionHelperTemplatedSample : Page
-{
- public TransitionHelperTemplatedSample()
- {
- this.InitializeComponent();
- }
-}
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml
deleted file mode 100644
index 477899de8..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml.cs
deleted file mode 100644
index fae83df31..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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.
-
-namespace TransitionHelperExperiment.Samples;
-
-[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)]
-// Single values without a colon are used for both label and value.
-// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value").
-[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")]
-[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")]
-[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground",
- "Teal : #0ddc8c",
- "Sand : #e7a676",
- "Dull green : #5d7577")]
-
-[ToolkitSample(id: nameof(TransitionHelperTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")]
-public sealed partial class TransitionHelperTemplatedStyleCustomSample : Page
-{
- public TransitionHelperTemplatedStyleCustomSample()
- {
- this.InitializeComponent();
- }
-}
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml
deleted file mode 100644
index f5476a392..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml.cs
deleted file mode 100644
index e22a4a199..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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.
-
-namespace TransitionHelperExperiment.Samples;
-
-[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)]
-// Single values without a colon are used for both label and value.
-// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value").
-[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")]
-[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")]
-[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground",
- "Teal : #0ddc8c",
- "Sand : #e7a676",
- "Dull green : #5d7577")]
-
-[ToolkitSample(id: nameof(TransitionHelperXbindBackedSample), "Backed templated control", description: "A sample for showing how to create and use a templated control with a backed resource dictionary.")]
-public sealed partial class TransitionHelperXbindBackedSample : Page
-{
- public TransitionHelperXbindBackedSample()
- {
- this.InitializeComponent();
- }
-}
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml
deleted file mode 100644
index 1cc8e6630..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml.cs
deleted file mode 100644
index a184806db..000000000
--- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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.
-
-namespace TransitionHelperExperiment.Samples;
-
-[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)]
-// Single values without a colon are used for both label and value.
-// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value").
-[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")]
-[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")]
-[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground",
- "Teal : #0ddc8c",
- "Sand : #e7a676",
- "Dull green : #5d7577")]
-
-[ToolkitSample(id: nameof(TransitionHelperXbindBackedStyleCustomSample), "Backed templated control (restyled)", description: "A sample for showing how to create and use a templated control with a backed resource dictionary and a custom style.")]
-public sealed partial class TransitionHelperXbindBackedStyleCustomSample : Page
-{
- public TransitionHelperXbindBackedStyleCustomSample()
- {
- this.InitializeComponent();
- }
-}
diff --git a/labs/TransitionHelper/src/Behaviors/ReverseTransitionAction.cs b/labs/TransitionHelper/src/Behaviors/ReverseTransitionAction.cs
new file mode 100644
index 000000000..2a42b6f79
--- /dev/null
+++ b/labs/TransitionHelper/src/Behaviors/ReverseTransitionAction.cs
@@ -0,0 +1,57 @@
+// 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 Microsoft.Xaml.Interactivity;
+
+#if WINAPPSDK
+using CommunityToolkit.WinUI.UI.Animations;
+#else
+using Microsoft.Toolkit.Uwp.UI.Animations;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// An implementation that can trigger a target instance.
+///
+public sealed partial class ReverseTransitionAction : DependencyObject, IAction
+{
+ ///
+ /// Gets or sets the linked instance to reverse.
+ ///
+ public TransitionHelper Transition
+ {
+ get
+ {
+ return (TransitionHelper)this.GetValue(TransitionProperty);
+ }
+
+ set
+ {
+ this.SetValue(TransitionProperty, value);
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty TransitionProperty = DependencyProperty.Register(
+ nameof(Transition),
+ typeof(TransitionHelper),
+ typeof(StartTransitionAction),
+ new PropertyMetadata(null));
+
+ ///
+ public object Execute(object sender, object parameter)
+ {
+ if (this.Transition is null)
+ {
+ throw new ArgumentNullException(nameof(this.Transition));
+ }
+
+ _ = this.Transition.ReverseAsync();
+
+ return null!;
+ }
+}
diff --git a/labs/TransitionHelper/src/Behaviors/StartTransitionAction.cs b/labs/TransitionHelper/src/Behaviors/StartTransitionAction.cs
new file mode 100644
index 000000000..fd4be2cea
--- /dev/null
+++ b/labs/TransitionHelper/src/Behaviors/StartTransitionAction.cs
@@ -0,0 +1,119 @@
+// 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 Microsoft.Xaml.Interactivity;
+
+#if WINAPPSDK
+using CommunityToolkit.WinUI.UI.Animations;
+#else
+using Microsoft.Toolkit.Uwp.UI.Animations;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// An implementation that can trigger a target instance.
+///
+public sealed partial class StartTransitionAction : DependencyObject, IAction
+{
+ ///
+ /// Gets or sets the linked instance to invoke.
+ ///
+ public TransitionHelper Transition
+ {
+ get
+ {
+ return (TransitionHelper)this.GetValue(TransitionProperty);
+ }
+
+ set
+ {
+ this.SetValue(TransitionProperty, value);
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty TransitionProperty = DependencyProperty.Register(
+ nameof(Transition),
+ typeof(TransitionHelper),
+ typeof(StartTransitionAction),
+ new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the source control of the .
+ ///
+ public FrameworkElement Source
+ {
+ get
+ {
+ return (FrameworkElement)this.GetValue(SourceProperty);
+ }
+
+ set
+ {
+ this.SetValue(SourceProperty, value);
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
+ nameof(Source),
+ typeof(FrameworkElement),
+ typeof(StartTransitionAction),
+ new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets the target control of the .
+ ///
+ public FrameworkElement Target
+ {
+ get
+ {
+ return (FrameworkElement)this.GetValue(TargetProperty);
+ }
+
+ set
+ {
+ this.SetValue(TargetProperty, value);
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty TargetProperty = DependencyProperty.Register(
+ nameof(Target),
+ typeof(FrameworkElement),
+ typeof(StartTransitionAction),
+ new PropertyMetadata(null));
+
+ ///
+ public object Execute(object sender, object parameter)
+ {
+ if (this.Transition is null)
+ {
+ throw new ArgumentNullException(nameof(this.Transition));
+ }
+
+ if (this.Source is null)
+ {
+ throw new ArgumentNullException(nameof(this.Source));
+ }
+
+ if (this.Target is null)
+ {
+ throw new ArgumentNullException(nameof(this.Target));
+ }
+
+ this.Transition.Source = this.Source;
+ this.Transition.Target = this.Target;
+ _ = this.Transition.StartAsync();
+
+ return null!;
+ }
+}
diff --git a/labs/TransitionHelper/src/Dependencies.props b/labs/TransitionHelper/src/Dependencies.props
index e622e1df4..9c7434348 100644
--- a/labs/TransitionHelper/src/Dependencies.props
+++ b/labs/TransitionHelper/src/Dependencies.props
@@ -11,21 +11,21 @@
-
+
-
+
-
+
-
+
-
+
diff --git a/labs/TransitionHelper/src/Enums/ScaleMode.cs b/labs/TransitionHelper/src/Enums/ScaleMode.cs
new file mode 100644
index 000000000..b75729fa2
--- /dev/null
+++ b/labs/TransitionHelper/src/Enums/ScaleMode.cs
@@ -0,0 +1,38 @@
+// 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.
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/TransitionHelper/src/Enums/VisualStateToggleMethod.cs b/labs/TransitionHelper/src/Enums/VisualStateToggleMethod.cs
new file mode 100644
index 000000000..ed37092c4
--- /dev/null
+++ b/labs/TransitionHelper/src/Enums/VisualStateToggleMethod.cs
@@ -0,0 +1,21 @@
+// 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.
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/TransitionHelper/src/IScalingCalculator.cs b/labs/TransitionHelper/src/IScalingCalculator.cs
new file mode 100644
index 000000000..a11b2cd95
--- /dev/null
+++ b/labs/TransitionHelper/src/IScalingCalculator.cs
@@ -0,0 +1,21 @@
+// 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;
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/TransitionHelper/src/Properties/IsExternalInit.cs b/labs/TransitionHelper/src/Properties/IsExternalInit.cs
new file mode 100644
index 000000000..0160c8ef0
--- /dev/null
+++ b/labs/TransitionHelper/src/Properties/IsExternalInit.cs
@@ -0,0 +1,16 @@
+// 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.ComponentModel;
+
+namespace System.Runtime.CompilerServices;
+
+///
+/// Reserved to be used by the compiler for tracking metadata.
+/// This class should not be used by developers in source code.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+internal static class IsExternalInit
+{
+}
diff --git a/labs/TransitionHelper/src/Themes/Generic.xaml b/labs/TransitionHelper/src/Themes/Generic.xaml
deleted file mode 100644
index b728504c7..000000000
--- a/labs/TransitionHelper/src/Themes/Generic.xaml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/labs/TransitionHelper/src/Toolkit/AnimationExtensions.cs b/labs/TransitionHelper/src/Toolkit/AnimationExtensions.cs
new file mode 100644
index 000000000..5088d40b5
--- /dev/null
+++ b/labs/TransitionHelper/src/Toolkit/AnimationExtensions.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.
+
+using System.Numerics;
+
+#if WINAPPSDK
+using CommunityToolkit.WinUI.UI.Animations;
+using Microsoft.UI.Xaml.Media.Animation;
+#else
+using Microsoft.Toolkit.Uwp.UI.Animations;
+using Windows.UI.Xaml.Media.Animation;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// Common properties related to extensions. (Copied from internal of Toolkit)
+///
+internal static class AnimationExtensions
+{
+ ///
+ /// The reusable mapping of control points for easing curves for combinations of and values.
+ ///
+ internal static readonly Dictionary<(EasingType Type, EasingMode Mode), (Vector2 A, Vector2 B)> EasingMaps = new()
+ {
+ // The default/inout combination is missing, as in this case we just skip creating
+ // an easing function entirely, and rely on the composition APIs using the implicit
+ // easing automatically. This is a bit more efficient, and results in the same
+ // visual behavior anyway, as that's the standard combination for animations.
+ [(EasingType.Default, EasingMode.EaseOut)] = (new(0.1f, 0.9f), new(0.2f, 1.0f)),
+ [(EasingType.Default, EasingMode.EaseIn)] = (new(0.7f, 0.0f), new(1.0f, 0.5f)),
+
+ [(EasingType.Cubic, EasingMode.EaseOut)] = (new(0.215f, 0.61f), new(0.355f, 1f)),
+ [(EasingType.Cubic, EasingMode.EaseIn)] = (new(0.55f, 0.055f), new(0.675f, 0.19f)),
+ [(EasingType.Cubic, EasingMode.EaseInOut)] = (new(0.645f, 0.045f), new(0.355f, 1f)),
+
+ [(EasingType.Back, EasingMode.EaseOut)] = (new(0.175f, 0.885f), new(0.32f, 1.275f)),
+ [(EasingType.Back, EasingMode.EaseIn)] = (new(0.6f, -0.28f), new(0.735f, 0.045f)),
+ [(EasingType.Back, EasingMode.EaseInOut)] = (new(0.68f, -0.55f), new(0.265f, 1.55f)),
+
+ [(EasingType.Bounce, EasingMode.EaseOut)] = (new(0.58f, 1.93f), new(.08f, .36f)),
+ [(EasingType.Bounce, EasingMode.EaseIn)] = (new(0.93f, 0.7f), new(0.4f, -0.93f)),
+ [(EasingType.Bounce, EasingMode.EaseInOut)] = (new(0.65f, -0.85f), new(0.35f, 1.85f)),
+
+ [(EasingType.Elastic, EasingMode.EaseOut)] = (new(0.37f, 2.68f), new(0f, 0.22f)),
+ [(EasingType.Elastic, EasingMode.EaseIn)] = (new(1, .78f), new(.63f, -1.68f)),
+ [(EasingType.Elastic, EasingMode.EaseInOut)] = (new(0.9f, -1.2f), new(0.1f, 2.2f)),
+
+ [(EasingType.Circle, EasingMode.EaseOut)] = (new(0.075f, 0.82f), new(0.165f, 1f)),
+ [(EasingType.Circle, EasingMode.EaseIn)] = (new(0.6f, 0.04f), new(0.98f, 0.335f)),
+ [(EasingType.Circle, EasingMode.EaseInOut)] = (new(0.785f, 0.135f), new(0.15f, 0.86f)),
+
+ [(EasingType.Quadratic, EasingMode.EaseOut)] = (new(0.25f, 0.46f), new(0.45f, 0.94f)),
+ [(EasingType.Quadratic, EasingMode.EaseIn)] = (new(0.55f, 0.085f), new(0.68f, 0.53f)),
+ [(EasingType.Quadratic, EasingMode.EaseInOut)] = (new(0.445f, 0.03f), new(0.515f, 0.955f)),
+
+ [(EasingType.Quartic, EasingMode.EaseOut)] = (new(0.165f, 0.84f), new(0.44f, 1f)),
+ [(EasingType.Quartic, EasingMode.EaseIn)] = (new(0.895f, 0.03f), new(0.685f, 0.22f)),
+ [(EasingType.Quartic, EasingMode.EaseInOut)] = (new(0.77f, 0.0f), new(0.175f, 1.0f)),
+
+ [(EasingType.Quintic, EasingMode.EaseOut)] = (new(0.23f, 1f), new(0.32f, 1f)),
+ [(EasingType.Quintic, EasingMode.EaseIn)] = (new(0.755f, 0.05f), new(0.855f, 0.06f)),
+ [(EasingType.Quintic, EasingMode.EaseInOut)] = (new(0.86f, 0.0f), new(0.07f, 1.0f)),
+
+ [(EasingType.Sine, EasingMode.EaseOut)] = (new(0.39f, 0.575f), new(0.565f, 1f)),
+ [(EasingType.Sine, EasingMode.EaseIn)] = (new(0.47f, 0.0f), new(0.745f, 0.715f)),
+ [(EasingType.Sine, EasingMode.EaseInOut)] = (new(0.445f, 0.05f), new(0.55f, 0.95f))
+ };
+}
diff --git a/labs/TransitionHelper/src/TransitionConfig.cs b/labs/TransitionHelper/src/TransitionConfig.cs
new file mode 100644
index 000000000..4b2166841
--- /dev/null
+++ b/labs/TransitionHelper/src/TransitionConfig.cs
@@ -0,0 +1,71 @@
+// 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.
+
+#if WINAPPSDK
+using CommunityToolkit.WinUI.UI.Animations;
+using Microsoft.UI.Xaml.Media.Animation;
+#else
+using Microsoft.Toolkit.Uwp.UI.Animations;
+using Windows.UI.Xaml.Media.Animation;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/TransitionHelper/src/TransitionHelper.Animation.cs b/labs/TransitionHelper/src/TransitionHelper.Animation.cs
new file mode 100644
index 000000000..56fcd27b2
--- /dev/null
+++ b/labs/TransitionHelper/src/TransitionHelper.Animation.cs
@@ -0,0 +1,446 @@
+// 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.Diagnostics.Contracts;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+#if WINAPPSDK
+using CommunityToolkit.WinUI.UI;
+using CommunityToolkit.WinUI.UI.Animations;
+using Microsoft.UI.Composition;
+using Microsoft.UI.Xaml.Hosting;
+using Microsoft.UI.Xaml.Media.Animation;
+using AnimationDirection = Microsoft.UI.Composition.AnimationDirection;
+#else
+using Microsoft.Toolkit.Uwp.UI;
+using Microsoft.Toolkit.Uwp.UI.Animations;
+using Windows.UI.Composition;
+using Windows.UI.Xaml.Hosting;
+using Windows.UI.Xaml.Media.Animation;
+using AnimationDirection = Windows.UI.Composition.AnimationDirection;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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