From 662c85d4c2c2d91952ae4c3fd5ccb7a78c1ff3e0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 27 Apr 2026 12:40:20 +0300 Subject: [PATCH] Implement animation for `RankedPlayBottomOrnament` (#37504) Based off of https://github.com/ppy/osu/pull/37478 with some improvements such as: * Simpler `progress` handling (single adjustable value instead of 2) * 2 less drawable paths (replaced with circlular containers) * Reworked remaining paths to have as little texture sizes as possible --------- Co-authored-by: Krzysztof Gutkowski Co-authored-by: Dean Herbert --- .../TestSceneRankedPlayBottomOrnament.cs | 53 ++++++ .../Matchmaking/RankedPlayBottomOrnament.cs | 177 +++++++++++------- 2 files changed, 159 insertions(+), 71 deletions(-) create mode 100644 osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayBottomOrnament.cs diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayBottomOrnament.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayBottomOrnament.cs new file mode 100644 index 0000000000..78bd992ea9 --- /dev/null +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayBottomOrnament.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Screens.OnlinePlay.Matchmaking; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.RankedPlay +{ + public partial class TestSceneRankedPlayBottomOrnament : OsuTestScene + { + private readonly TestOrnament ornament; + + public TestSceneRankedPlayBottomOrnament() + { + Child = new Container + { + Width = 400, + Height = 24, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray, + }, + ornament = new TestOrnament(), + } + }; + } + + [Test] + public void TestAnimations() + { + AddStep("hide", () => ornament.Hide()); + AddStep("show", () => ornament.Show()); + AddSliderStep("Progress", 0f, 1f, 0f, p => ornament.Progress = p); + } + + private partial class TestOrnament : RankedPlayBottomOrnament + { + public new float Progress + { + set => base.Progress = value; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs index 6eaa4ae6ff..d2d9b56bf5 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs @@ -1,15 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Lines; -using osu.Framework.Layout; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; @@ -28,24 +30,59 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private const int width = 400; private const int height = 24; + protected override bool StartHidden => true; protected override bool BlockPositionalInput => false; + private readonly SliderPath sliderPath; + private Path pathLeft = null!; private Path pathRight = null!; - private Path pathCenter = null!; - private Path pathCenterWide = null!; + private Circle centerLine = null!; + private Circle centerLineThick = null!; - private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize); + private readonly Bindable progressBindable = new Bindable(); - protected override bool StartHidden => true; + protected float Progress + { + get => progressBindable.Value; + set => progressBindable.Value = value; + } + + public RankedPlayBottomOrnament() + { + const int top = 2; // to account for the middle segment being twice as wide + const int bottom = 10; + const int curve_smoothness = 5; + + const int left_start = 0; + const int left_corner = 10; + const int left_end = 20; + var diagonalDirLeft = (new Vector2(left_start, bottom) - new Vector2(left_corner, top)).Normalized(); + + const float right_start = width; + const float right_corner = right_start - 10; + const float right_end = right_start - 20; + var diagonalDirRight = (new Vector2(right_start, bottom) - new Vector2(right_corner, top)).Normalized(); + + sliderPath = new SliderPath(new[] + { + new PathControlPoint(new Vector2(left_start, bottom), PathType.BEZIER), + new PathControlPoint(new Vector2(left_corner, top) + diagonalDirLeft * curve_smoothness), + new PathControlPoint(new Vector2(left_corner, top)), + new PathControlPoint(new Vector2(left_end, top), PathType.LINEAR), + new PathControlPoint(new Vector2(right_end, top), PathType.BEZIER), + new PathControlPoint(new Vector2(right_corner, top)), + new PathControlPoint(new Vector2(right_corner, top) + diagonalDirRight * curve_smoothness), + new PathControlPoint(new Vector2(right_start, bottom)), + }); + } [BackgroundDependencyLoader] private void load() { Width = width; Height = height; - Alpha = 0; Masking = true; @@ -70,26 +107,26 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { pathLeft = new SmoothPath { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, + Origin = Anchor.BottomRight, PathRadius = 1, }, - pathCenter = new SmoothPath + centerLine = new Circle { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, - PathRadius = 1, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 1, + Height = 2, }, - pathCenterWide = new SmoothPath + centerLineThick = new Circle { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, - PathRadius = 2, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = 4, }, pathRight = new SmoothPath { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, PathRadius = 1, }, }, @@ -106,73 +143,71 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking }; } - private void recomputePaths() + protected override void LoadComplete() { - const int top = 2; // to account for the middle segment being twice as wide - const int bottom = 10; - const int curve_smoothness = 5; - - pathCenter.AddVertex(new Vector2(30, top)); - pathCenter.AddVertex(new Vector2(DrawWidth - 30, top)); - - pathCenterWide.AddVertex(new Vector2(60, top)); - pathCenterWide.AddVertex(new Vector2(DrawWidth - 60, top)); - - const int left_start = 0; - const int left_corner = 10; - const int left_end = 20; - - List vertices = new List(); - var diagonalDirLeft = (new Vector2(left_start, bottom) - new Vector2(left_corner, top)).Normalized(); - - var sliderPathLeft = new SliderPath(new[] - { - new PathControlPoint(new Vector2(left_start, bottom), PathType.LINEAR), - new PathControlPoint(new Vector2(left_corner, top) + diagonalDirLeft * curve_smoothness, PathType.BEZIER), - new PathControlPoint(new Vector2(left_corner, top)), - new PathControlPoint(new Vector2(left_end, top), PathType.LINEAR), - }); - - sliderPathLeft.GetPathToProgress(vertices, 0.0, 1.0); - pathLeft.Vertices = vertices; - - float rightStart = DrawWidth; - float rightCorner = rightStart - 10; - float rightEnd = rightStart - 20; - - var diagonalDirRight = (new Vector2(rightStart, bottom) - new Vector2(rightCorner, top)).Normalized(); - var sliderPathRight = new SliderPath(new[] - { - new PathControlPoint(new Vector2(rightStart, bottom), PathType.LINEAR), - new PathControlPoint(new Vector2(rightCorner, top) + diagonalDirRight * curve_smoothness, PathType.BEZIER), - new PathControlPoint(new Vector2(rightCorner, top)), - new PathControlPoint(new Vector2(rightEnd, top), PathType.LINEAR), - }); - - sliderPathRight.GetPathToProgress(vertices, 0.0, 1.0); - pathRight.Vertices = vertices; + base.LoadComplete(); + progressBindable.BindValueChanged(progress => recomputePaths(progress.NewValue), true); } - protected override void Update() - { - base.Update(); + private readonly List vertices = new List(); - if (!layout.IsValid) + private void recomputePaths(float newProgress) + { + centerLineThick.Width = Math.Clamp(newProgress, 0f, 0.7f); + centerLine.Width = Math.Clamp(newProgress, 0f, 0.85f); + + if (newProgress > 0.9f) { - recomputePaths(); - layout.Validate(); + pathLeft.Alpha = 1; + pathRight.Alpha = 1; + + vertices.Clear(); + sliderPath.GetPathToProgress(vertices, 0.5f - newProgress * 0.5f, 0.05f); + + Vector2 lastVertex = vertices[^1]; + Vector2 firstVertex = vertices[0]; + for (int i = 0; i < vertices.Count; i++) + vertices[i] -= firstVertex; + + pathLeft.Vertices = vertices; + pathLeft.Position = pathLeft.PositionInBoundingBox(lastVertex); + + vertices.Clear(); + sliderPath.GetPathToProgress(vertices, 0.95f, 0.5f + newProgress * 0.5f); + + firstVertex = vertices[0]; + for (int i = 0; i < vertices.Count; i++) + vertices[i] -= firstVertex; + + pathRight.Vertices = vertices; + pathRight.Position = pathRight.PositionInBoundingBox(firstVertex) - new Vector2(pathRight.PathRadius * 2); + } + else + { + pathLeft.Alpha = 0; + pathRight.Alpha = 0; } } + private const int duration = 1200; + private const Easing easing = Easing.OutExpo; + protected override void PopIn() { - this.FadeIn(500, Easing.OutQuint); - // TODO: animate this better. + this.MoveToY(5) + .Delay(550) + .MoveToY(0, duration - 550, easing); + + this.FadeIn(duration, easing) + .TransformTo(nameof(Progress), 1f, duration, easing); } protected override void PopOut() { - this.FadeOut(500, Easing.OutQuint); + this.MoveToY(5, duration / 2f, Easing.In); + + this.FadeOut(duration, easing) + .TransformTo(nameof(Progress), 0f, duration, easing); } } }