| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Threading;
- using System.Threading.Tasks;
- using Avalonia;
- using Avalonia.Animation;
- using Avalonia.Animation.Easings;
- using Avalonia.Media;
- namespace ControlCatalog.Pages.Transitions;
- /// <summary>
- /// Transitions between two pages using a wave clip that reveals the next page.
- /// </summary>
- public class WaveRevealPageTransition : PageSlide
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
- /// </summary>
- public WaveRevealPageTransition()
- {
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
- /// </summary>
- /// <param name="duration">The duration of the animation.</param>
- /// <param name="orientation">The axis on which the animation should occur.</param>
- public WaveRevealPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal)
- : base(duration, orientation)
- {
- }
- /// <summary>
- /// Gets or sets the maximum wave bulge (pixels) along the movement axis.
- /// </summary>
- public double MaxBulge { get; set; } = 120.0;
- /// <summary>
- /// Gets or sets the bulge factor along the movement axis (0-1).
- /// </summary>
- public double BulgeFactor { get; set; } = 0.35;
- /// <summary>
- /// Gets or sets the bulge factor along the cross axis (0-1).
- /// </summary>
- public double CrossBulgeFactor { get; set; } = 0.3;
- /// <summary>
- /// Gets or sets a cross-axis offset (pixels) to shift the wave center.
- /// </summary>
- public double WaveCenterOffset { get; set; } = 0.0;
- /// <summary>
- /// Gets or sets how strongly the wave center follows the provided offset.
- /// </summary>
- public double CenterSensitivity { get; set; } = 1.0;
- /// <summary>
- /// Gets or sets the bulge exponent used to shape the wave (1.0 = linear).
- /// Higher values tighten the bulge; lower values broaden it.
- /// </summary>
- public double BulgeExponent { get; set; } = 1.0;
- /// <summary>
- /// Gets or sets the easing applied to the wave progress (clip only).
- /// </summary>
- public Easing WaveEasing { get; set; } = new CubicEaseOut();
- /// <inheritdoc />
- public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- return;
- }
- if (to != null)
- {
- to.IsVisible = true;
- to.ZIndex = 1;
- }
- if (from != null)
- {
- from.ZIndex = 0;
- }
- await AnimateProgress(0.0, 1.0, from, to, forward, cancellationToken);
- if (to != null && !cancellationToken.IsCancellationRequested)
- {
- to.Clip = null;
- }
- if (from != null && !cancellationToken.IsCancellationRequested)
- {
- from.IsVisible = false;
- }
- }
- /// <inheritdoc />
- public override void Update(
- double progress,
- Visual? from,
- Visual? to,
- bool forward,
- double pageLength,
- IReadOnlyList<PageTransitionItem> visibleItems)
- {
- if (visibleItems.Count > 0)
- {
- UpdateVisibleItems(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 centerOffset = WaveCenterOffset * CenterSensitivity;
- var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
- if (to != null)
- {
- to.IsVisible = progress > 0.0;
- to.ZIndex = 1;
- to.Opacity = 1;
- if (progress >= 1.0)
- {
- to.Clip = null;
- }
- else
- {
- var waveProgress = WaveEasing?.Ease(progress) ?? progress;
- var clip = LiquidSwipeClipper.CreateWavePath(
- waveProgress,
- size,
- centerOffset,
- forward,
- isHorizontal,
- MaxBulge,
- BulgeFactor,
- CrossBulgeFactor,
- BulgeExponent);
- to.Clip = clip;
- }
- }
- if (from != null)
- {
- from.IsVisible = true;
- from.ZIndex = 0;
- from.Opacity = 1;
- }
- }
- private void UpdateVisibleItems(
- Visual? from,
- Visual? to,
- bool forward,
- double pageLength,
- IReadOnlyList<PageTransitionItem> visibleItems)
- {
- if (from is null && to is null)
- return;
- var parent = GetVisualParent(from, to);
- var size = parent.Bounds.Size;
- var centerOffset = WaveCenterOffset * CenterSensitivity;
- var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
- var resolvedPageLength = pageLength > 0
- ? pageLength
- : (isHorizontal ? size.Width : size.Height);
- foreach (var item in visibleItems)
- {
- var visual = item.Visual;
- visual.IsVisible = true;
- visual.Opacity = 1;
- visual.Clip = null;
- visual.ZIndex = ReferenceEquals(visual, to) ? 1 : 0;
- if (!ReferenceEquals(visual, to))
- continue;
- var visibleFraction = GetVisibleFraction(item.ViewportCenterOffset, size, resolvedPageLength, isHorizontal);
- if (visibleFraction >= 1.0)
- continue;
- visual.Clip = LiquidSwipeClipper.CreateWavePath(
- visibleFraction,
- size,
- centerOffset,
- forward,
- isHorizontal,
- MaxBulge,
- BulgeFactor,
- CrossBulgeFactor,
- BulgeExponent);
- }
- }
- private static double GetVisibleFraction(double offsetFromCenter, Size viewportSize, double pageLength, bool isHorizontal)
- {
- if (pageLength <= 0)
- return 1.0;
- var viewportLength = isHorizontal ? viewportSize.Width : viewportSize.Height;
- if (viewportLength <= 0)
- return 0.0;
- var viewportUnits = viewportLength / pageLength;
- var edgePeek = Math.Max(0.0, (viewportUnits - 1.0) / 2.0);
- return Math.Clamp(1.0 + edgePeek - Math.Abs(offsetFromCenter), 0.0, 1.0);
- }
- /// <inheritdoc />
- public override void Reset(Visual visual)
- {
- visual.Clip = null;
- visual.ZIndex = 0;
- visual.Opacity = 1;
- }
- private async Task AnimateProgress(
- double from,
- double to,
- Visual? fromVisual,
- Visual? toVisual,
- bool forward,
- CancellationToken cancellationToken)
- {
- var parent = GetVisualParent(fromVisual, toVisual);
- var pageLength = Orientation == PageSlide.SlideAxis.Horizontal
- ? parent.Bounds.Width
- : parent.Bounds.Height;
- var durationMs = Math.Max(Duration.TotalMilliseconds * Math.Abs(to - from), 50);
- var startTicks = Stopwatch.GetTimestamp();
- var tickFreq = Stopwatch.Frequency;
- while (!cancellationToken.IsCancellationRequested)
- {
- var elapsedMs = (Stopwatch.GetTimestamp() - startTicks) * 1000.0 / tickFreq;
- var t = Math.Clamp(elapsedMs / durationMs, 0.0, 1.0);
- var eased = SlideInEasing?.Ease(t) ?? t;
- var progress = from + (to - from) * eased;
- Update(progress, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>());
- if (t >= 1.0)
- break;
- await Task.Delay(16, cancellationToken);
- }
- if (!cancellationToken.IsCancellationRequested)
- {
- Update(to, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>());
- }
- }
- private static class LiquidSwipeClipper
- {
- public static Geometry CreateWavePath(
- double progress,
- Size size,
- double waveCenterOffset,
- bool forward,
- bool isHorizontal,
- double maxBulge,
- double bulgeFactor,
- double crossBulgeFactor,
- double bulgeExponent)
- {
- var width = size.Width;
- var height = size.Height;
- if (progress <= 0)
- return new RectangleGeometry(new Rect(0, 0, 0, 0));
- if (progress >= 1)
- return new RectangleGeometry(new Rect(0, 0, width, height));
- if (width <= 0 || height <= 0)
- return new RectangleGeometry(new Rect(0, 0, 0, 0));
- var mainLength = isHorizontal ? width : height;
- var crossLength = isHorizontal ? height : width;
- var wavePhase = Math.Sin(progress * Math.PI);
- var bulgeProgress = bulgeExponent == 1.0 ? wavePhase : Math.Pow(wavePhase, bulgeExponent);
- var revealedLength = mainLength * progress;
- var bulgeMain = Math.Min(mainLength * bulgeFactor, maxBulge) * bulgeProgress;
- bulgeMain = Math.Min(bulgeMain, revealedLength * 0.45);
- var bulgeCross = crossLength * crossBulgeFactor;
- var waveCenter = crossLength / 2 + waveCenterOffset;
- waveCenter = Math.Clamp(waveCenter, bulgeCross, crossLength - bulgeCross);
- var geometry = new StreamGeometry();
- using (var context = geometry.Open())
- {
- if (isHorizontal)
- {
- if (forward)
- {
- var waveX = width * (1 - progress);
- context.BeginFigure(new Point(width, 0), true);
- context.LineTo(new Point(waveX, 0));
- context.CubicBezierTo(
- new Point(waveX, waveCenter - bulgeCross),
- new Point(waveX - bulgeMain, waveCenter - bulgeCross * 0.5),
- new Point(waveX - bulgeMain, waveCenter));
- context.CubicBezierTo(
- new Point(waveX - bulgeMain, waveCenter + bulgeCross * 0.5),
- new Point(waveX, waveCenter + bulgeCross),
- new Point(waveX, height));
- context.LineTo(new Point(width, height));
- context.EndFigure(true);
- }
- else
- {
- var waveX = width * progress;
- context.BeginFigure(new Point(0, 0), true);
- context.LineTo(new Point(waveX, 0));
- context.CubicBezierTo(
- new Point(waveX, waveCenter - bulgeCross),
- new Point(waveX + bulgeMain, waveCenter - bulgeCross * 0.5),
- new Point(waveX + bulgeMain, waveCenter));
- context.CubicBezierTo(
- new Point(waveX + bulgeMain, waveCenter + bulgeCross * 0.5),
- new Point(waveX, waveCenter + bulgeCross),
- new Point(waveX, height));
- context.LineTo(new Point(0, height));
- context.EndFigure(true);
- }
- }
- else
- {
- if (forward)
- {
- var waveY = height * (1 - progress);
- context.BeginFigure(new Point(0, height), true);
- context.LineTo(new Point(0, waveY));
- context.CubicBezierTo(
- new Point(waveCenter - bulgeCross, waveY),
- new Point(waveCenter - bulgeCross * 0.5, waveY - bulgeMain),
- new Point(waveCenter, waveY - bulgeMain));
- context.CubicBezierTo(
- new Point(waveCenter + bulgeCross * 0.5, waveY - bulgeMain),
- new Point(waveCenter + bulgeCross, waveY),
- new Point(width, waveY));
- context.LineTo(new Point(width, height));
- context.EndFigure(true);
- }
- else
- {
- var waveY = height * progress;
- context.BeginFigure(new Point(0, 0), true);
- context.LineTo(new Point(0, waveY));
- context.CubicBezierTo(
- new Point(waveCenter - bulgeCross, waveY),
- new Point(waveCenter - bulgeCross * 0.5, waveY + bulgeMain),
- new Point(waveCenter, waveY + bulgeMain));
- context.CubicBezierTo(
- new Point(waveCenter + bulgeCross * 0.5, waveY + bulgeMain),
- new Point(waveCenter + bulgeCross, waveY),
- new Point(width, waveY));
- context.LineTo(new Point(width, 0));
- context.EndFigure(true);
- }
- }
- }
- return geometry;
- }
- }
- }
|