using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
namespace ControlCatalog.Pages.Transitions;
///
/// Transitions between two pages with a card-stack effect:
/// the top page moves/rotates away while the next page scales up underneath.
///
public class CardStackPageTransition : PageSlide
{
private const double ViewportLiftScale = 0.03;
private const double ViewportPromotionScale = 0.02;
private const double ViewportDepthOpacityFalloff = 0.08;
private const double SidePeekAngle = 4.0;
private const double FarPeekAngle = 7.0;
///
/// Initializes a new instance of the class.
///
public CardStackPageTransition()
{
}
///
/// Initializes a new instance of the class.
///
/// The duration of the animation.
/// The axis on which the animation should occur.
public CardStackPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal)
: base(duration, orientation)
{
}
///
/// Gets or sets the maximum rotation angle (degrees) applied to the top card.
///
public double MaxSwipeAngle { get; set; } = 15.0;
///
/// Gets or sets the scale reduction applied to the back card (0.05 = 5%).
///
public double BackCardScale { get; set; } = 0.05;
///
/// Gets or sets the vertical offset (pixels) applied to the back card.
///
public double BackCardOffset { get; set; } = 0.0;
///
public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
var tasks = new List();
var parent = GetVisualParent(from, to);
var distance = Orientation == PageSlide.SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
var translateProperty = Orientation == PageSlide.SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
var rotationTarget = Orientation == PageSlide.SlideAxis.Horizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
var startScale = 1.0 - BackCardScale;
if (from != null)
{
var (rotate, translate) = EnsureTopTransforms(from);
rotate.Angle = 0;
translate.X = 0;
translate.Y = 0;
from.Opacity = 1;
from.ZIndex = 1;
var animation = new Animation
{
Easing = SlideOutEasing,
Duration = Duration,
FillMode = FillMode,
Children =
{
new KeyFrame
{
Setters =
{
new Setter(translateProperty, 0d),
new Setter(RotateTransform.AngleProperty, 0d)
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter(translateProperty, forward ? -distance : distance),
new Setter(RotateTransform.AngleProperty, rotationTarget)
},
Cue = new Cue(1d)
}
}
};
tasks.Add(animation.RunAsync(from, cancellationToken));
}
if (to != null)
{
var (scale, translate) = EnsureBackTransforms(to);
scale.ScaleX = startScale;
scale.ScaleY = startScale;
translate.X = 0;
translate.Y = BackCardOffset;
to.IsVisible = true;
to.Opacity = 1;
to.ZIndex = 0;
var animation = new Animation
{
Easing = SlideInEasing,
Duration = Duration,
FillMode = FillMode,
Children =
{
new KeyFrame
{
Setters =
{
new Setter(ScaleTransform.ScaleXProperty, startScale),
new Setter(ScaleTransform.ScaleYProperty, startScale),
new Setter(TranslateTransform.YProperty, BackCardOffset)
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter(ScaleTransform.ScaleXProperty, 1d),
new Setter(ScaleTransform.ScaleYProperty, 1d),
new Setter(TranslateTransform.YProperty, 0d)
},
Cue = new Cue(1d)
}
}
};
tasks.Add(animation.RunAsync(to, cancellationToken));
}
await Task.WhenAll(tasks);
if (from != null && !cancellationToken.IsCancellationRequested)
{
from.IsVisible = false;
}
if (!cancellationToken.IsCancellationRequested && to != null)
{
var (scale, translate) = EnsureBackTransforms(to);
scale.ScaleX = 1;
scale.ScaleY = 1;
translate.X = 0;
translate.Y = 0;
}
}
///
public override void Update(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList visibleItems)
{
if (visibleItems.Count > 0)
{
UpdateVisibleItems(progress, from, to, forward, pageLength, visibleItems);
return;
}
if (from is null && to is null)
return;
var parent = GetVisualParent(from, to);
var size = parent.Bounds.Size;
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
var distance = pageLength > 0
? pageLength
: (isHorizontal ? size.Width : size.Height);
var rotationTarget = isHorizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0;
var startScale = 1.0 - BackCardScale;
if (from != null)
{
var (rotate, translate) = EnsureTopTransforms(from);
if (isHorizontal)
{
translate.X = forward ? -distance * progress : distance * progress;
translate.Y = 0;
}
else
{
translate.X = 0;
translate.Y = forward ? -distance * progress : distance * progress;
}
rotate.Angle = rotationTarget * progress;
from.IsVisible = true;
from.Opacity = 1;
from.ZIndex = 1;
}
if (to != null)
{
var (scale, translate) = EnsureBackTransforms(to);
var currentScale = startScale + (1.0 - startScale) * progress;
var currentOffset = BackCardOffset * (1.0 - progress);
scale.ScaleX = currentScale;
scale.ScaleY = currentScale;
if (isHorizontal)
{
translate.X = 0;
translate.Y = currentOffset;
}
else
{
translate.X = currentOffset;
translate.Y = 0;
}
to.IsVisible = true;
to.Opacity = 1;
to.ZIndex = 0;
}
}
///
public override void Reset(Visual visual)
{
visual.RenderTransform = null;
visual.RenderTransformOrigin = default;
visual.Opacity = 1;
visual.ZIndex = 0;
}
private void UpdateVisibleItems(
double progress,
Visual? from,
Visual? to,
bool forward,
double pageLength,
IReadOnlyList visibleItems)
{
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
var rotationTarget = isHorizontal
? (forward ? -MaxSwipeAngle : MaxSwipeAngle)
: 0.0;
var stackOffset = GetViewportStackOffset(pageLength);
var lift = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI);
foreach (var item in visibleItems)
{
var visual = item.Visual;
var (rotate, scale, translate) = EnsureViewportTransforms(visual);
var depth = GetViewportDepth(item.ViewportCenterOffset);
var scaleValue = Math.Max(0.84, 1.0 - (BackCardScale * depth));
var stackValue = stackOffset * depth;
var baseOpacity = Math.Max(0.8, 1.0 - (ViewportDepthOpacityFalloff * depth));
var restingAngle = isHorizontal ? GetViewportRestingAngle(item.ViewportCenterOffset) : 0.0;
rotate.Angle = restingAngle;
scale.ScaleX = scaleValue;
scale.ScaleY = scaleValue;
translate.X = 0;
translate.Y = 0;
if (ReferenceEquals(visual, from))
{
rotate.Angle = restingAngle + (rotationTarget * progress);
stackValue -= stackOffset * 0.2 * lift;
baseOpacity = Math.Min(1.0, baseOpacity + 0.08);
}
if (ReferenceEquals(visual, to))
{
var promotedScale = Math.Min(1.0, scaleValue + (ViewportLiftScale * lift) + (ViewportPromotionScale * progress));
scale.ScaleX = promotedScale;
scale.ScaleY = promotedScale;
rotate.Angle = restingAngle * (1.0 - progress);
stackValue = Math.Max(0.0, stackValue - (stackOffset * (0.45 + (0.2 * lift)) * progress));
baseOpacity = Math.Min(1.0, baseOpacity + (0.12 * lift));
}
if (isHorizontal)
translate.Y = stackValue;
else
translate.X = stackValue;
visual.IsVisible = true;
visual.Opacity = baseOpacity;
visual.ZIndex = GetViewportZIndex(item.ViewportCenterOffset, visual, from, to);
}
}
private static (RotateTransform rotate, TranslateTransform translate) EnsureTopTransforms(Visual visual)
{
if (visual.RenderTransform is TransformGroup group &&
group.Children.Count == 2 &&
group.Children[0] is RotateTransform rotateTransform &&
group.Children[1] is TranslateTransform translateTransform)
{
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotateTransform, translateTransform);
}
var rotate = new RotateTransform();
var translate = new TranslateTransform();
visual.RenderTransform = new TransformGroup
{
Children =
{
rotate,
translate
}
};
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotate, translate);
}
private static (ScaleTransform scale, TranslateTransform translate) EnsureBackTransforms(Visual visual)
{
if (visual.RenderTransform is TransformGroup group &&
group.Children.Count == 2 &&
group.Children[0] is ScaleTransform scaleTransform &&
group.Children[1] is TranslateTransform translateTransform)
{
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (scaleTransform, translateTransform);
}
var scale = new ScaleTransform();
var translate = new TranslateTransform();
visual.RenderTransform = new TransformGroup
{
Children =
{
scale,
translate
}
};
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (scale, translate);
}
private static (RotateTransform rotate, ScaleTransform scale, TranslateTransform translate) EnsureViewportTransforms(Visual visual)
{
if (visual.RenderTransform is TransformGroup group &&
group.Children.Count == 3 &&
group.Children[0] is RotateTransform rotateTransform &&
group.Children[1] is ScaleTransform scaleTransform &&
group.Children[2] is TranslateTransform translateTransform)
{
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotateTransform, scaleTransform, translateTransform);
}
var rotate = new RotateTransform();
var scale = new ScaleTransform(1, 1);
var translate = new TranslateTransform();
visual.RenderTransform = new TransformGroup
{
Children =
{
rotate,
scale,
translate
}
};
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
return (rotate, scale, translate);
}
private double GetViewportStackOffset(double pageLength)
{
if (BackCardOffset > 0)
return BackCardOffset;
return Math.Clamp(pageLength * 0.045, 10.0, 18.0);
}
private static double GetViewportDepth(double offsetFromCenter)
{
var distance = Math.Abs(offsetFromCenter);
if (distance <= 1.0)
return distance;
if (distance <= 2.0)
return 1.0 + ((distance - 1.0) * 0.8);
return 1.8;
}
private static double GetViewportRestingAngle(double offsetFromCenter)
{
var sign = Math.Sign(offsetFromCenter);
if (sign == 0)
return 0;
var distance = Math.Abs(offsetFromCenter);
if (distance <= 1.0)
return sign * Lerp(0.0, SidePeekAngle, distance);
if (distance <= 2.0)
return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0);
return sign * FarPeekAngle;
}
private static double Lerp(double from, double to, double t)
{
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0));
}
private static int GetViewportZIndex(double offsetFromCenter, Visual visual, Visual? from, Visual? to)
{
if (ReferenceEquals(visual, from))
return 5;
if (ReferenceEquals(visual, to))
return 4;
var distance = Math.Abs(offsetFromCenter);
if (distance < 0.5)
return 4;
if (distance < 1.5)
return 3;
return 2;
}
}