660 lines
24 KiB
C#
660 lines
24 KiB
C#
namespace MarketAlly.SpotlightTour.Maui.Animations;
|
|
|
|
/// <summary>
|
|
/// Helper class for executing tour animations on visual elements.
|
|
/// </summary>
|
|
public static class AnimationHelper
|
|
{
|
|
private static readonly Random _random = new();
|
|
|
|
/// <summary>
|
|
/// Converts AnimationEasing enum to MAUI Easing.
|
|
/// </summary>
|
|
public static Easing ToMauiEasing(AnimationEasing easing)
|
|
{
|
|
return easing switch
|
|
{
|
|
AnimationEasing.Linear => Easing.Linear,
|
|
AnimationEasing.CubicIn => Easing.CubicIn,
|
|
AnimationEasing.CubicOut => Easing.CubicOut,
|
|
AnimationEasing.CubicInOut => Easing.CubicInOut,
|
|
AnimationEasing.SpringOut => Easing.SpringOut,
|
|
AnimationEasing.BounceOut => Easing.BounceOut,
|
|
AnimationEasing.SinIn => Easing.SinIn,
|
|
AnimationEasing.SinOut => Easing.SinOut,
|
|
AnimationEasing.Elastic => CreateElasticEasing(),
|
|
_ => Easing.CubicInOut
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a custom elastic easing function.
|
|
/// </summary>
|
|
private static Easing CreateElasticEasing()
|
|
{
|
|
return new Easing(t =>
|
|
{
|
|
if (t == 0 || t == 1) return t;
|
|
var p = 0.3;
|
|
var s = p / 4;
|
|
return Math.Pow(2, -10 * t) * Math.Sin((t - s) * (2 * Math.PI) / p) + 1;
|
|
});
|
|
}
|
|
|
|
#region Entrance Animations
|
|
|
|
/// <summary>
|
|
/// Plays an entrance animation on an element.
|
|
/// </summary>
|
|
public static async Task PlayEntranceAsync(
|
|
VisualElement element,
|
|
EntranceAnimation animation,
|
|
AnimationConfiguration config,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Ensure we're on the main thread for UI operations
|
|
if (!MainThread.IsMainThread)
|
|
{
|
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
|
PlayEntranceAsync(element, animation, config, cancellationToken));
|
|
return;
|
|
}
|
|
|
|
// Capture the target position BEFORE any modifications
|
|
// This preserves positions set by corner positioning, etc.
|
|
var targetX = element.TranslationX;
|
|
var targetY = element.TranslationY;
|
|
|
|
if (animation == EntranceAnimation.None || config.EntranceDurationMs == 0)
|
|
{
|
|
element.Opacity = 1;
|
|
element.Scale = 1;
|
|
// Preserve the target position instead of resetting to 0
|
|
element.TranslationX = targetX;
|
|
element.TranslationY = targetY;
|
|
element.IsVisible = true;
|
|
return;
|
|
}
|
|
|
|
var duration = config.EntranceDurationMs;
|
|
var easing = ToMauiEasing(config.EntranceEasing);
|
|
|
|
// Set initial state (with offset from target position)
|
|
SetEntranceInitialState(element, animation, config, targetX, targetY);
|
|
element.IsVisible = true;
|
|
|
|
// Apply delay if configured
|
|
if (config.EntranceDelayMs > 0)
|
|
{
|
|
await Task.Delay((int)config.EntranceDelayMs, cancellationToken);
|
|
}
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
// Animate to final state (the target position)
|
|
await AnimateEntranceAsync(element, animation, duration, easing, config, targetX, targetY);
|
|
}
|
|
|
|
private static void SetEntranceInitialState(VisualElement element, EntranceAnimation animation, AnimationConfiguration config, double targetX, double targetY)
|
|
{
|
|
element.Opacity = 0;
|
|
element.Scale = 1;
|
|
// Start at the target position (preserves corner positioning, etc.)
|
|
element.TranslationX = targetX;
|
|
element.TranslationY = targetY;
|
|
element.RotationY = 0;
|
|
|
|
switch (animation)
|
|
{
|
|
case EntranceAnimation.FadeIn:
|
|
// Just opacity change
|
|
break;
|
|
|
|
case EntranceAnimation.SlideFromLeft:
|
|
element.TranslationX = targetX - config.SlideDistance;
|
|
break;
|
|
|
|
case EntranceAnimation.SlideFromRight:
|
|
element.TranslationX = targetX + config.SlideDistance;
|
|
break;
|
|
|
|
case EntranceAnimation.SlideFromTop:
|
|
element.TranslationY = targetY - config.SlideDistance;
|
|
break;
|
|
|
|
case EntranceAnimation.SlideFromBottom:
|
|
case EntranceAnimation.SlideUpFade:
|
|
element.TranslationY = targetY + config.SlideDistance;
|
|
break;
|
|
|
|
case EntranceAnimation.SlideDownFade:
|
|
element.TranslationY = targetY - config.SlideDistance;
|
|
break;
|
|
|
|
case EntranceAnimation.ScaleUp:
|
|
case EntranceAnimation.PopIn:
|
|
element.Scale = config.ScaleDownFactor;
|
|
break;
|
|
|
|
case EntranceAnimation.ScaleDown:
|
|
element.Scale = config.ScaleUpFactor;
|
|
break;
|
|
|
|
case EntranceAnimation.BounceIn:
|
|
case EntranceAnimation.ZoomIn:
|
|
element.Scale = 0;
|
|
break;
|
|
|
|
case EntranceAnimation.FlipIn:
|
|
element.RotationY = -90;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static async Task AnimateEntranceAsync(
|
|
VisualElement element,
|
|
EntranceAnimation animation,
|
|
uint duration,
|
|
Easing easing,
|
|
AnimationConfiguration config,
|
|
double targetX,
|
|
double targetY)
|
|
{
|
|
var tasks = new List<Task>();
|
|
|
|
switch (animation)
|
|
{
|
|
case EntranceAnimation.FadeIn:
|
|
tasks.Add(element.FadeTo(1, duration, easing));
|
|
break;
|
|
|
|
case EntranceAnimation.SlideFromLeft:
|
|
case EntranceAnimation.SlideFromRight:
|
|
tasks.Add(element.FadeTo(1, duration, easing));
|
|
tasks.Add(element.TranslateTo(targetX, targetY, duration, easing));
|
|
break;
|
|
|
|
case EntranceAnimation.SlideFromTop:
|
|
case EntranceAnimation.SlideFromBottom:
|
|
tasks.Add(element.FadeTo(1, duration, easing));
|
|
tasks.Add(element.TranslateTo(targetX, targetY, duration, easing));
|
|
break;
|
|
|
|
case EntranceAnimation.SlideUpFade:
|
|
case EntranceAnimation.SlideDownFade:
|
|
tasks.Add(element.FadeTo(1, duration, easing));
|
|
tasks.Add(element.TranslateTo(targetX, targetY, duration, easing));
|
|
break;
|
|
|
|
case EntranceAnimation.ScaleUp:
|
|
case EntranceAnimation.ScaleDown:
|
|
tasks.Add(element.FadeTo(1, duration, easing));
|
|
tasks.Add(element.ScaleTo(1, duration, easing));
|
|
break;
|
|
|
|
case EntranceAnimation.BounceIn:
|
|
tasks.Add(element.FadeTo(1, duration / 2, easing));
|
|
tasks.Add(AnimateBounceInAsync(element, duration));
|
|
break;
|
|
|
|
case EntranceAnimation.ZoomIn:
|
|
tasks.Add(element.FadeTo(1, duration, easing));
|
|
tasks.Add(AnimateZoomInAsync(element, duration, config));
|
|
break;
|
|
|
|
case EntranceAnimation.PopIn:
|
|
tasks.Add(element.FadeTo(1, duration, easing));
|
|
tasks.Add(AnimatePopInAsync(element, duration, config));
|
|
break;
|
|
|
|
case EntranceAnimation.FlipIn:
|
|
tasks.Add(element.FadeTo(1, duration / 2, easing));
|
|
tasks.Add(element.RotateYTo(0, duration, easing));
|
|
break;
|
|
}
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task AnimateBounceInAsync(VisualElement element, uint duration)
|
|
{
|
|
var animation = new Animation();
|
|
animation.Add(0, 0.6, new Animation(v => element.Scale = v, 0, 1.15, Easing.CubicOut));
|
|
animation.Add(0.6, 0.8, new Animation(v => element.Scale = v, 1.15, 0.9, Easing.CubicInOut));
|
|
animation.Add(0.8, 1.0, new Animation(v => element.Scale = v, 0.9, 1.0, Easing.CubicOut));
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
animation.Commit(element, "BounceIn", length: duration, finished: (_, _) => tcs.SetResult(true));
|
|
await tcs.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task AnimateZoomInAsync(VisualElement element, uint duration, AnimationConfiguration config)
|
|
{
|
|
var animation = new Animation();
|
|
animation.Add(0, 0.7, new Animation(v => element.Scale = v, 0, config.ScaleUpFactor, Easing.CubicOut));
|
|
animation.Add(0.7, 1.0, new Animation(v => element.Scale = v, config.ScaleUpFactor, 1.0, Easing.CubicInOut));
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
animation.Commit(element, "ZoomIn", length: duration, finished: (_, _) => tcs.SetResult(true));
|
|
await tcs.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task AnimatePopInAsync(VisualElement element, uint duration, AnimationConfiguration config)
|
|
{
|
|
var animation = new Animation();
|
|
animation.Add(0, 0.5, new Animation(v => element.Scale = v, config.ScaleDownFactor, 1.05, Easing.CubicOut));
|
|
animation.Add(0.5, 1.0, new Animation(v => element.Scale = v, 1.05, 1.0, Easing.CubicInOut));
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
animation.Commit(element, "PopIn", length: duration, finished: (_, _) => tcs.SetResult(true));
|
|
await tcs.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exit Animations
|
|
|
|
/// <summary>
|
|
/// Plays an exit animation on an element.
|
|
/// </summary>
|
|
public static async Task PlayExitAsync(
|
|
VisualElement element,
|
|
ExitAnimation animation,
|
|
AnimationConfiguration config,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Ensure we're on the main thread for UI operations
|
|
if (!MainThread.IsMainThread)
|
|
{
|
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
|
PlayExitAsync(element, animation, config, cancellationToken));
|
|
return;
|
|
}
|
|
|
|
if (animation == ExitAnimation.None || config.ExitDurationMs == 0)
|
|
{
|
|
element.IsVisible = false;
|
|
return;
|
|
}
|
|
|
|
var duration = config.ExitDurationMs;
|
|
var easing = ToMauiEasing(config.ExitEasing);
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
await AnimateExitAsync(element, animation, duration, easing, config);
|
|
|
|
element.IsVisible = false;
|
|
|
|
// Reset properties for next use
|
|
element.Opacity = 1;
|
|
element.Scale = 1;
|
|
element.TranslationX = 0;
|
|
element.TranslationY = 0;
|
|
}
|
|
|
|
private static async Task AnimateExitAsync(
|
|
VisualElement element,
|
|
ExitAnimation animation,
|
|
uint duration,
|
|
Easing easing,
|
|
AnimationConfiguration config)
|
|
{
|
|
var tasks = new List<Task>();
|
|
|
|
switch (animation)
|
|
{
|
|
case ExitAnimation.FadeOut:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
break;
|
|
|
|
case ExitAnimation.SlideToLeft:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
tasks.Add(element.TranslateTo(-config.SlideDistance, element.TranslationY, duration, easing));
|
|
break;
|
|
|
|
case ExitAnimation.SlideToRight:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
tasks.Add(element.TranslateTo(config.SlideDistance, element.TranslationY, duration, easing));
|
|
break;
|
|
|
|
case ExitAnimation.SlideToTop:
|
|
case ExitAnimation.SlideUpFade:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
tasks.Add(element.TranslateTo(element.TranslationX, -config.SlideDistance, duration, easing));
|
|
break;
|
|
|
|
case ExitAnimation.SlideToBottom:
|
|
case ExitAnimation.SlideDownFade:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
tasks.Add(element.TranslateTo(element.TranslationX, config.SlideDistance, duration, easing));
|
|
break;
|
|
|
|
case ExitAnimation.ScaleDown:
|
|
case ExitAnimation.PopOut:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
tasks.Add(element.ScaleTo(config.ScaleDownFactor, duration, easing));
|
|
break;
|
|
|
|
case ExitAnimation.ScaleUp:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
tasks.Add(element.ScaleTo(config.ScaleUpFactor, duration, easing));
|
|
break;
|
|
|
|
case ExitAnimation.ZoomOut:
|
|
tasks.Add(element.FadeTo(0, duration, easing));
|
|
tasks.Add(element.ScaleTo(0, duration, easing));
|
|
break;
|
|
}
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Spotlight Effects
|
|
|
|
/// <summary>
|
|
/// Starts a spotlight effect animation.
|
|
/// </summary>
|
|
/// <param name="element">The spotlight overlay element.</param>
|
|
/// <param name="effect">The effect to apply.</param>
|
|
/// <param name="config">Animation configuration.</param>
|
|
/// <param name="cancellationToken">Cancellation token to stop the effect.</param>
|
|
public static async Task PlaySpotlightEffectAsync(
|
|
VisualElement element,
|
|
SpotlightEffect effect,
|
|
AnimationConfiguration config,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (effect == SpotlightEffect.None)
|
|
return;
|
|
|
|
var repeatCount = config.SpotlightEffectRepeat;
|
|
var iterations = 0;
|
|
|
|
while (!cancellationToken.IsCancellationRequested && (repeatCount == 0 || iterations < repeatCount))
|
|
{
|
|
iterations++;
|
|
|
|
try
|
|
{
|
|
switch (effect)
|
|
{
|
|
case SpotlightEffect.Pulse:
|
|
await PlayPulseEffectAsync(element, config, cancellationToken).ConfigureAwait(false);
|
|
break;
|
|
|
|
case SpotlightEffect.Breathe:
|
|
await PlayBreatheEffectAsync(element, config, cancellationToken).ConfigureAwait(false);
|
|
break;
|
|
|
|
case SpotlightEffect.Glow:
|
|
await PlayGlowEffectAsync(element, config, cancellationToken).ConfigureAwait(false);
|
|
break;
|
|
|
|
case SpotlightEffect.Shimmer:
|
|
await PlayShimmerEffectAsync(element, config, cancellationToken).ConfigureAwait(false);
|
|
break;
|
|
|
|
case SpotlightEffect.Ripple:
|
|
await PlayRippleEffectAsync(element, config, cancellationToken).ConfigureAwait(false);
|
|
break;
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task PlayPulseEffectAsync(VisualElement element, AnimationConfiguration config, CancellationToken ct)
|
|
{
|
|
var duration = config.PulseEffectDurationMs / 2;
|
|
var intensity = config.SpotlightEffectIntensity;
|
|
|
|
// Scale up
|
|
await element.ScaleTo(1 + intensity * 0.05, duration, Easing.SinInOut).ConfigureAwait(false);
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
// Scale back
|
|
await element.ScaleTo(1, duration, Easing.SinInOut).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task PlayBreatheEffectAsync(VisualElement element, AnimationConfiguration config, CancellationToken ct)
|
|
{
|
|
var duration = config.PulseEffectDurationMs;
|
|
var intensity = config.SpotlightEffectIntensity;
|
|
|
|
// Slow breathe in
|
|
await element.ScaleTo(1 + intensity * 0.03, duration / 2, Easing.SinInOut).ConfigureAwait(false);
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
// Hold briefly
|
|
await Task.Delay(100, ct).ConfigureAwait(false);
|
|
|
|
// Slow breathe out
|
|
await element.ScaleTo(1, duration / 2, Easing.SinInOut).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task PlayGlowEffectAsync(VisualElement element, AnimationConfiguration config, CancellationToken ct)
|
|
{
|
|
var duration = config.PulseEffectDurationMs / 2;
|
|
var baseOpacity = element.Opacity;
|
|
var glowAmount = config.SpotlightEffectIntensity * 0.2;
|
|
|
|
// Brighten
|
|
await element.FadeTo(Math.Min(1, baseOpacity + glowAmount), duration, Easing.SinInOut).ConfigureAwait(false);
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
// Return to normal
|
|
await element.FadeTo(baseOpacity, duration, Easing.SinInOut).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task PlayShimmerEffectAsync(VisualElement element, AnimationConfiguration config, CancellationToken ct)
|
|
{
|
|
var duration = config.PulseEffectDurationMs / 4;
|
|
|
|
// Quick opacity fluctuation
|
|
for (int i = 0; i < 3 && !ct.IsCancellationRequested; i++)
|
|
{
|
|
var targetOpacity = 0.85 + (_random.NextDouble() * 0.15);
|
|
await element.FadeTo(targetOpacity, duration, Easing.Linear).ConfigureAwait(false);
|
|
}
|
|
|
|
await element.FadeTo(1, duration, Easing.Linear).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task PlayRippleEffectAsync(VisualElement element, AnimationConfiguration config, CancellationToken ct)
|
|
{
|
|
var duration = config.PulseEffectDurationMs;
|
|
var intensity = config.SpotlightEffectIntensity;
|
|
|
|
// Create ripple effect using scale animation
|
|
var animation = new Animation();
|
|
animation.Add(0, 0.5, new Animation(v => element.Scale = v, 1, 1 + intensity * 0.08, Easing.CubicOut));
|
|
animation.Add(0.5, 1.0, new Animation(v => element.Scale = v, 1 + intensity * 0.08, 1, Easing.CubicIn));
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
animation.Commit(element, "Ripple", length: duration, finished: (_, _) => tcs.SetResult(true));
|
|
|
|
using var registration = ct.Register(() => tcs.TrySetCanceled());
|
|
await tcs.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Step Transitions
|
|
|
|
/// <summary>
|
|
/// Animates transition between steps for a callout element.
|
|
/// </summary>
|
|
public static async Task AnimateStepTransitionAsync(
|
|
VisualElement element,
|
|
StepTransition transition,
|
|
bool isForward,
|
|
AnimationConfiguration config,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// Ensure we're on the main thread for UI operations
|
|
if (!MainThread.IsMainThread)
|
|
{
|
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
|
AnimateStepTransitionAsync(element, transition, isForward, config, cancellationToken));
|
|
return;
|
|
}
|
|
|
|
// No animation for None transition - just return immediately
|
|
if (transition == StepTransition.None)
|
|
return;
|
|
|
|
var duration = config.StepTransitionDurationMs;
|
|
var easing = ToMauiEasing(config.SpotlightEasing);
|
|
|
|
switch (transition)
|
|
{
|
|
case StepTransition.Crossfade:
|
|
await AnimateCrossfadeAsync(element, duration, easing);
|
|
break;
|
|
|
|
case StepTransition.Slide:
|
|
case StepTransition.Push:
|
|
await AnimateSlideTransitionAsync(element, isForward, duration, easing, config);
|
|
break;
|
|
|
|
case StepTransition.Flip:
|
|
await AnimateFlipTransitionAsync(element, isForward, duration, easing);
|
|
break;
|
|
|
|
default:
|
|
await AnimateCrossfadeAsync(element, duration, easing);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static async Task AnimateCrossfadeAsync(VisualElement element, uint duration, Easing easing)
|
|
{
|
|
// Quick fade out and in
|
|
await element.FadeTo(0.3, duration / 2, easing).ConfigureAwait(false);
|
|
await element.FadeTo(1, duration / 2, easing).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task AnimateSlideTransitionAsync(
|
|
VisualElement element,
|
|
bool isForward,
|
|
uint duration,
|
|
Easing easing,
|
|
AnimationConfiguration config)
|
|
{
|
|
var direction = isForward ? 1 : -1;
|
|
var distance = config.SlideDistance * direction;
|
|
|
|
// Save original position to restore after animation
|
|
var originalX = element.TranslationX;
|
|
var originalY = element.TranslationY;
|
|
|
|
// Slide out (relative to current position)
|
|
await Task.WhenAll(
|
|
element.FadeTo(0, duration / 2, easing),
|
|
element.TranslateTo(originalX + distance, originalY, duration / 2, easing)
|
|
);
|
|
|
|
// Reset position to opposite side (relative to original)
|
|
element.TranslationX = originalX - distance;
|
|
|
|
// Slide in back to original position
|
|
await Task.WhenAll(
|
|
element.FadeTo(1, duration / 2, easing),
|
|
element.TranslateTo(originalX, originalY, duration / 2, easing)
|
|
);
|
|
}
|
|
|
|
private static async Task AnimateFlipTransitionAsync(
|
|
VisualElement element,
|
|
bool isForward,
|
|
uint duration,
|
|
Easing easing)
|
|
{
|
|
var direction = isForward ? 1 : -1;
|
|
|
|
// Flip out
|
|
await element.RotateYTo(90 * direction, duration / 2, easing).ConfigureAwait(false);
|
|
|
|
// Flip in from opposite side
|
|
element.RotationY = -90 * direction;
|
|
await element.RotateYTo(0, duration / 2, easing).ConfigureAwait(false);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utility Methods
|
|
|
|
/// <summary>
|
|
/// Cancels all animations on an element.
|
|
/// </summary>
|
|
public static void CancelAnimations(VisualElement element)
|
|
{
|
|
element.CancelAnimations();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets element to default visual state.
|
|
/// </summary>
|
|
public static void ResetElement(VisualElement element)
|
|
{
|
|
// Ensure we're on the main thread for UI operations
|
|
if (!MainThread.IsMainThread)
|
|
{
|
|
MainThread.BeginInvokeOnMainThread(() => ResetElement(element));
|
|
return;
|
|
}
|
|
|
|
element.CancelAnimations();
|
|
element.Opacity = 1;
|
|
element.Scale = 1;
|
|
element.TranslationX = 0;
|
|
element.TranslationY = 0;
|
|
element.RotationY = 0;
|
|
element.Rotation = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a shake animation for error feedback.
|
|
/// </summary>
|
|
public static async Task ShakeAsync(VisualElement element, uint duration = 400)
|
|
{
|
|
var animation = new Animation();
|
|
animation.Add(0, 0.125, new Animation(v => element.TranslationX = v, 0, -10, Easing.Linear));
|
|
animation.Add(0.125, 0.25, new Animation(v => element.TranslationX = v, -10, 10, Easing.Linear));
|
|
animation.Add(0.25, 0.375, new Animation(v => element.TranslationX = v, 10, -10, Easing.Linear));
|
|
animation.Add(0.375, 0.5, new Animation(v => element.TranslationX = v, -10, 10, Easing.Linear));
|
|
animation.Add(0.5, 0.625, new Animation(v => element.TranslationX = v, 10, -5, Easing.Linear));
|
|
animation.Add(0.625, 0.75, new Animation(v => element.TranslationX = v, -5, 5, Easing.Linear));
|
|
animation.Add(0.75, 0.875, new Animation(v => element.TranslationX = v, 5, -2, Easing.Linear));
|
|
animation.Add(0.875, 1.0, new Animation(v => element.TranslationX = v, -2, 0, Easing.Linear));
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
animation.Commit(element, "Shake", length: duration, finished: (_, _) => tcs.SetResult(true));
|
|
await tcs.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a heartbeat/attention animation.
|
|
/// </summary>
|
|
public static async Task HeartbeatAsync(VisualElement element, uint duration = 800)
|
|
{
|
|
var animation = new Animation();
|
|
animation.Add(0, 0.14, new Animation(v => element.Scale = v, 1, 1.1, Easing.CubicOut));
|
|
animation.Add(0.14, 0.28, new Animation(v => element.Scale = v, 1.1, 1, Easing.CubicIn));
|
|
animation.Add(0.42, 0.56, new Animation(v => element.Scale = v, 1, 1.1, Easing.CubicOut));
|
|
animation.Add(0.56, 0.70, new Animation(v => element.Scale = v, 1.1, 1, Easing.CubicIn));
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
animation.Commit(element, "Heartbeat", length: duration, finished: (_, _) => tcs.SetResult(true));
|
|
await tcs.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
#endregion
|
|
}
|