WaveRevealPageTransition.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using Avalonia;
  7. using Avalonia.Animation;
  8. using Avalonia.Animation.Easings;
  9. using Avalonia.Media;
  10. namespace ControlCatalog.Pages.Transitions;
  11. /// <summary>
  12. /// Transitions between two pages using a wave clip that reveals the next page.
  13. /// </summary>
  14. public class WaveRevealPageTransition : PageSlide
  15. {
  16. /// <summary>
  17. /// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
  18. /// </summary>
  19. public WaveRevealPageTransition()
  20. {
  21. }
  22. /// <summary>
  23. /// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
  24. /// </summary>
  25. /// <param name="duration">The duration of the animation.</param>
  26. /// <param name="orientation">The axis on which the animation should occur.</param>
  27. public WaveRevealPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal)
  28. : base(duration, orientation)
  29. {
  30. }
  31. /// <summary>
  32. /// Gets or sets the maximum wave bulge (pixels) along the movement axis.
  33. /// </summary>
  34. public double MaxBulge { get; set; } = 120.0;
  35. /// <summary>
  36. /// Gets or sets the bulge factor along the movement axis (0-1).
  37. /// </summary>
  38. public double BulgeFactor { get; set; } = 0.35;
  39. /// <summary>
  40. /// Gets or sets the bulge factor along the cross axis (0-1).
  41. /// </summary>
  42. public double CrossBulgeFactor { get; set; } = 0.3;
  43. /// <summary>
  44. /// Gets or sets a cross-axis offset (pixels) to shift the wave center.
  45. /// </summary>
  46. public double WaveCenterOffset { get; set; } = 0.0;
  47. /// <summary>
  48. /// Gets or sets how strongly the wave center follows the provided offset.
  49. /// </summary>
  50. public double CenterSensitivity { get; set; } = 1.0;
  51. /// <summary>
  52. /// Gets or sets the bulge exponent used to shape the wave (1.0 = linear).
  53. /// Higher values tighten the bulge; lower values broaden it.
  54. /// </summary>
  55. public double BulgeExponent { get; set; } = 1.0;
  56. /// <summary>
  57. /// Gets or sets the easing applied to the wave progress (clip only).
  58. /// </summary>
  59. public Easing WaveEasing { get; set; } = new CubicEaseOut();
  60. /// <inheritdoc />
  61. public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
  62. {
  63. if (cancellationToken.IsCancellationRequested)
  64. {
  65. return;
  66. }
  67. if (to != null)
  68. {
  69. to.IsVisible = true;
  70. to.ZIndex = 1;
  71. }
  72. if (from != null)
  73. {
  74. from.ZIndex = 0;
  75. }
  76. await AnimateProgress(0.0, 1.0, from, to, forward, cancellationToken);
  77. if (to != null && !cancellationToken.IsCancellationRequested)
  78. {
  79. to.Clip = null;
  80. }
  81. if (from != null && !cancellationToken.IsCancellationRequested)
  82. {
  83. from.IsVisible = false;
  84. }
  85. }
  86. /// <inheritdoc />
  87. public override void Update(
  88. double progress,
  89. Visual? from,
  90. Visual? to,
  91. bool forward,
  92. double pageLength,
  93. IReadOnlyList<PageTransitionItem> visibleItems)
  94. {
  95. if (visibleItems.Count > 0)
  96. {
  97. UpdateVisibleItems(from, to, forward, pageLength, visibleItems);
  98. return;
  99. }
  100. if (from is null && to is null)
  101. return;
  102. var parent = GetVisualParent(from, to);
  103. var size = parent.Bounds.Size;
  104. var centerOffset = WaveCenterOffset * CenterSensitivity;
  105. var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
  106. if (to != null)
  107. {
  108. to.IsVisible = progress > 0.0;
  109. to.ZIndex = 1;
  110. to.Opacity = 1;
  111. if (progress >= 1.0)
  112. {
  113. to.Clip = null;
  114. }
  115. else
  116. {
  117. var waveProgress = WaveEasing?.Ease(progress) ?? progress;
  118. var clip = LiquidSwipeClipper.CreateWavePath(
  119. waveProgress,
  120. size,
  121. centerOffset,
  122. forward,
  123. isHorizontal,
  124. MaxBulge,
  125. BulgeFactor,
  126. CrossBulgeFactor,
  127. BulgeExponent);
  128. to.Clip = clip;
  129. }
  130. }
  131. if (from != null)
  132. {
  133. from.IsVisible = true;
  134. from.ZIndex = 0;
  135. from.Opacity = 1;
  136. }
  137. }
  138. private void UpdateVisibleItems(
  139. Visual? from,
  140. Visual? to,
  141. bool forward,
  142. double pageLength,
  143. IReadOnlyList<PageTransitionItem> visibleItems)
  144. {
  145. if (from is null && to is null)
  146. return;
  147. var parent = GetVisualParent(from, to);
  148. var size = parent.Bounds.Size;
  149. var centerOffset = WaveCenterOffset * CenterSensitivity;
  150. var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal;
  151. var resolvedPageLength = pageLength > 0
  152. ? pageLength
  153. : (isHorizontal ? size.Width : size.Height);
  154. foreach (var item in visibleItems)
  155. {
  156. var visual = item.Visual;
  157. visual.IsVisible = true;
  158. visual.Opacity = 1;
  159. visual.Clip = null;
  160. visual.ZIndex = ReferenceEquals(visual, to) ? 1 : 0;
  161. if (!ReferenceEquals(visual, to))
  162. continue;
  163. var visibleFraction = GetVisibleFraction(item.ViewportCenterOffset, size, resolvedPageLength, isHorizontal);
  164. if (visibleFraction >= 1.0)
  165. continue;
  166. visual.Clip = LiquidSwipeClipper.CreateWavePath(
  167. visibleFraction,
  168. size,
  169. centerOffset,
  170. forward,
  171. isHorizontal,
  172. MaxBulge,
  173. BulgeFactor,
  174. CrossBulgeFactor,
  175. BulgeExponent);
  176. }
  177. }
  178. private static double GetVisibleFraction(double offsetFromCenter, Size viewportSize, double pageLength, bool isHorizontal)
  179. {
  180. if (pageLength <= 0)
  181. return 1.0;
  182. var viewportLength = isHorizontal ? viewportSize.Width : viewportSize.Height;
  183. if (viewportLength <= 0)
  184. return 0.0;
  185. var viewportUnits = viewportLength / pageLength;
  186. var edgePeek = Math.Max(0.0, (viewportUnits - 1.0) / 2.0);
  187. return Math.Clamp(1.0 + edgePeek - Math.Abs(offsetFromCenter), 0.0, 1.0);
  188. }
  189. /// <inheritdoc />
  190. public override void Reset(Visual visual)
  191. {
  192. visual.Clip = null;
  193. visual.ZIndex = 0;
  194. visual.Opacity = 1;
  195. }
  196. private async Task AnimateProgress(
  197. double from,
  198. double to,
  199. Visual? fromVisual,
  200. Visual? toVisual,
  201. bool forward,
  202. CancellationToken cancellationToken)
  203. {
  204. var parent = GetVisualParent(fromVisual, toVisual);
  205. var pageLength = Orientation == PageSlide.SlideAxis.Horizontal
  206. ? parent.Bounds.Width
  207. : parent.Bounds.Height;
  208. var durationMs = Math.Max(Duration.TotalMilliseconds * Math.Abs(to - from), 50);
  209. var startTicks = Stopwatch.GetTimestamp();
  210. var tickFreq = Stopwatch.Frequency;
  211. while (!cancellationToken.IsCancellationRequested)
  212. {
  213. var elapsedMs = (Stopwatch.GetTimestamp() - startTicks) * 1000.0 / tickFreq;
  214. var t = Math.Clamp(elapsedMs / durationMs, 0.0, 1.0);
  215. var eased = SlideInEasing?.Ease(t) ?? t;
  216. var progress = from + (to - from) * eased;
  217. Update(progress, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>());
  218. if (t >= 1.0)
  219. break;
  220. await Task.Delay(16, cancellationToken);
  221. }
  222. if (!cancellationToken.IsCancellationRequested)
  223. {
  224. Update(to, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>());
  225. }
  226. }
  227. private static class LiquidSwipeClipper
  228. {
  229. public static Geometry CreateWavePath(
  230. double progress,
  231. Size size,
  232. double waveCenterOffset,
  233. bool forward,
  234. bool isHorizontal,
  235. double maxBulge,
  236. double bulgeFactor,
  237. double crossBulgeFactor,
  238. double bulgeExponent)
  239. {
  240. var width = size.Width;
  241. var height = size.Height;
  242. if (progress <= 0)
  243. return new RectangleGeometry(new Rect(0, 0, 0, 0));
  244. if (progress >= 1)
  245. return new RectangleGeometry(new Rect(0, 0, width, height));
  246. if (width <= 0 || height <= 0)
  247. return new RectangleGeometry(new Rect(0, 0, 0, 0));
  248. var mainLength = isHorizontal ? width : height;
  249. var crossLength = isHorizontal ? height : width;
  250. var wavePhase = Math.Sin(progress * Math.PI);
  251. var bulgeProgress = bulgeExponent == 1.0 ? wavePhase : Math.Pow(wavePhase, bulgeExponent);
  252. var revealedLength = mainLength * progress;
  253. var bulgeMain = Math.Min(mainLength * bulgeFactor, maxBulge) * bulgeProgress;
  254. bulgeMain = Math.Min(bulgeMain, revealedLength * 0.45);
  255. var bulgeCross = crossLength * crossBulgeFactor;
  256. var waveCenter = crossLength / 2 + waveCenterOffset;
  257. waveCenter = Math.Clamp(waveCenter, bulgeCross, crossLength - bulgeCross);
  258. var geometry = new StreamGeometry();
  259. using (var context = geometry.Open())
  260. {
  261. if (isHorizontal)
  262. {
  263. if (forward)
  264. {
  265. var waveX = width * (1 - progress);
  266. context.BeginFigure(new Point(width, 0), true);
  267. context.LineTo(new Point(waveX, 0));
  268. context.CubicBezierTo(
  269. new Point(waveX, waveCenter - bulgeCross),
  270. new Point(waveX - bulgeMain, waveCenter - bulgeCross * 0.5),
  271. new Point(waveX - bulgeMain, waveCenter));
  272. context.CubicBezierTo(
  273. new Point(waveX - bulgeMain, waveCenter + bulgeCross * 0.5),
  274. new Point(waveX, waveCenter + bulgeCross),
  275. new Point(waveX, height));
  276. context.LineTo(new Point(width, height));
  277. context.EndFigure(true);
  278. }
  279. else
  280. {
  281. var waveX = width * progress;
  282. context.BeginFigure(new Point(0, 0), true);
  283. context.LineTo(new Point(waveX, 0));
  284. context.CubicBezierTo(
  285. new Point(waveX, waveCenter - bulgeCross),
  286. new Point(waveX + bulgeMain, waveCenter - bulgeCross * 0.5),
  287. new Point(waveX + bulgeMain, waveCenter));
  288. context.CubicBezierTo(
  289. new Point(waveX + bulgeMain, waveCenter + bulgeCross * 0.5),
  290. new Point(waveX, waveCenter + bulgeCross),
  291. new Point(waveX, height));
  292. context.LineTo(new Point(0, height));
  293. context.EndFigure(true);
  294. }
  295. }
  296. else
  297. {
  298. if (forward)
  299. {
  300. var waveY = height * (1 - progress);
  301. context.BeginFigure(new Point(0, height), true);
  302. context.LineTo(new Point(0, waveY));
  303. context.CubicBezierTo(
  304. new Point(waveCenter - bulgeCross, waveY),
  305. new Point(waveCenter - bulgeCross * 0.5, waveY - bulgeMain),
  306. new Point(waveCenter, waveY - bulgeMain));
  307. context.CubicBezierTo(
  308. new Point(waveCenter + bulgeCross * 0.5, waveY - bulgeMain),
  309. new Point(waveCenter + bulgeCross, waveY),
  310. new Point(width, waveY));
  311. context.LineTo(new Point(width, height));
  312. context.EndFigure(true);
  313. }
  314. else
  315. {
  316. var waveY = height * progress;
  317. context.BeginFigure(new Point(0, 0), true);
  318. context.LineTo(new Point(0, waveY));
  319. context.CubicBezierTo(
  320. new Point(waveCenter - bulgeCross, waveY),
  321. new Point(waveCenter - bulgeCross * 0.5, waveY + bulgeMain),
  322. new Point(waveCenter, waveY + bulgeMain));
  323. context.CubicBezierTo(
  324. new Point(waveCenter + bulgeCross * 0.5, waveY + bulgeMain),
  325. new Point(waveCenter + bulgeCross, waveY),
  326. new Point(width, waveY));
  327. context.LineTo(new Point(width, 0));
  328. context.EndFigure(true);
  329. }
  330. }
  331. }
  332. return geometry;
  333. }
  334. }
  335. }