diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 936808f38b..52b728a115 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 35e7742172..95b96adab0 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index c1044965b5..d12403016d 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 35e7742172..95b96adab0 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 3f926ed45a..1f4544098b 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -27,7 +27,7 @@ - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index d62d422f33..f47b069373 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index c9db824615..5a2e8e0bf0 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -3,7 +3,7 @@ - + WinExe diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 0d7b03d830..be51dc0e4c 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -3,7 +3,7 @@ - + WinExe diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 1eb1c85d93..c10c3ffb15 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -4,7 +4,7 @@ - + WinExe diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs index d88752f025..f99d4275bd 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Argon { @@ -51,6 +50,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private Container centre = null!; private CircularContainer fill = null!; + private Container ticksContainer = null!; + private ArgonSpinnerTicks ticks = null!; + [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject) { @@ -69,41 +71,85 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - fill = new CircularContainer + new Container { - Name = @"Fill", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Masking = true, - EdgeEffect = new EdgeEffectParameters + Padding = new MarginPadding(8f), + Children = new[] { - Type = EdgeEffectType.Shadow, - Colour = Colour4.FromHex("FC618F").Opacity(1f), - Radius = 40, + fill = new CircularContainer + { + Name = @"Fill", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Colour4.FromHex("FC618F").Opacity(1f), + Radius = 40, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0f, + AlwaysPresent = true, + } + }, + ticksContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = ticks = new ArgonSpinnerTicks(), + } }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0f, - AlwaysPresent = true, - } }, - new CircularContainer + new Container { Name = @"Ring", Masking = true, - BorderColour = Color4.White, - BorderThickness = 5, RelativeSizeAxes = Axes.Both, - Child = new Box + Padding = new MarginPadding(8f), + Children = new[] { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, + new ArgonSpinnerRingArc + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Name = "Top Arc", + }, + new ArgonSpinnerRingArc + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Name = "Bottom Arc", + Scale = new Vector2(1, -1), + }, + } + }, + new Container + { + Name = @"Sides", + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new ArgonSpinnerProgressArc + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Name = "Left Bar" + }, + new ArgonSpinnerProgressArc + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Name = "Right Bar", + Scale = new Vector2(-1, 1), + }, } }, - new ArgonSpinnerTicks(), } }, centre = new Container @@ -169,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon float targetScale = initial_fill_scale + (0.98f - initial_fill_scale) * drawableSpinner.Progress; fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); - disc.Rotation = drawableSpinner.RotationTracker.Rotation; + ticks.Rotation = drawableSpinner.RotationTracker.Rotation; } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -182,35 +228,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) { this.ScaleTo(initial_scale); - this.RotateTo(0); + ticksContainer.RotateTo(0); + centre.ScaleTo(0); + disc.ScaleTo(0); using (BeginDelayedSequence(spinner.TimePreempt / 2)) { // constant ambient rotation to give the spinner "spinning" character. - this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); + ticksContainer.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); } - using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset)) - { - switch (state) - { - case ArmedState.Hit: - this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out); - this.RotateTo(Rotation + 180, 320); - break; - - case ArmedState.Miss: - this.ScaleTo(initial_scale * 0.8f, 320, Easing.In); - break; - } - } - } - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) - { - centre.ScaleTo(0); - disc.ScaleTo(0); - using (BeginDelayedSequence(spinner.TimePreempt / 2)) { centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); @@ -222,6 +249,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon disc.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); } } + + using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset)) + { + switch (state) + { + case ArmedState.Hit: + disc.ScaleTo(initial_scale * 1.2f, 320, Easing.Out); + ticksContainer.RotateTo(ticksContainer.Rotation + 180, 320); + break; + + case ArmedState.Miss: + disc.ScaleTo(initial_scale * 0.8f, 320, Easing.In); + break; + } + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs new file mode 100644 index 0000000000..e998f55755 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs @@ -0,0 +1,71 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Skinning.Argon +{ + public class ArgonSpinnerProgressArc : CompositeDrawable + { + private const float arc_fill = 0.15f; + private const float arc_radius = 0.12f; + + private CircularProgress fill = null!; + + private DrawableSpinner spinner = null!; + + private CircularProgress background = null!; + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject) + { + RelativeSizeAxes = Axes.Both; + + spinner = (DrawableSpinner)drawableHitObject; + + InternalChildren = new Drawable[] + { + background = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White.Opacity(0.25f), + RelativeSizeAxes = Axes.Both, + Current = { Value = arc_fill }, + Rotation = 90 - arc_fill * 180, + InnerRadius = arc_radius, + RoundedCaps = true, + }, + fill = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + InnerRadius = arc_radius, + RoundedCaps = true, + } + }; + } + + protected override void Update() + { + base.Update(); + + background.Alpha = spinner.Progress >= 1 ? 0 : 1; + + fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed)); + fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed)); + + fill.Rotation = (float)(90 - fill.Current.Value * 180); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs new file mode 100644 index 0000000000..57fb57a09e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.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 System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Skinning.Argon +{ + public class ArgonSpinnerRingArc : CompositeDrawable + { + private const float arc_fill = 0.31f; + private const float arc_fill_complete = 0.50f; + + private const float arc_radius = 0.02f; + + private DrawableSpinner spinner = null!; + private CircularProgress fill = null!; + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject) + { + RelativeSizeAxes = Axes.Both; + + spinner = (DrawableSpinner)drawableHitObject; + InternalChild = fill = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Current = { Value = arc_fill }, + Rotation = -arc_fill * 180, + InnerRadius = arc_radius, + RoundedCaps = true, + }; + } + + protected override void Update() + { + base.Update(); + + fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed)); + fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed)); + + fill.Rotation = (float)(-fill.Current.Value * 180); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 38e61f5624..6af1beff69 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -3,7 +3,7 @@ - + WinExe diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs new file mode 100644 index 0000000000..28a9d17882 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs @@ -0,0 +1,190 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneBezierConverter : OsuTestScene + { + private readonly SmoothPath drawablePath; + private readonly SmoothPath controlPointDrawablePath; + private readonly SmoothPath convertedDrawablePath; + private readonly SmoothPath convertedControlPointDrawablePath; + + private SliderPath path = null!; + private SliderPath convertedPath = null!; + + public TestSceneBezierConverter() + { + Children = new Drawable[] + { + new Container + { + Children = + new Drawable[] + { + drawablePath = new SmoothPath(), + controlPointDrawablePath = new SmoothPath + { + Colour = Colour4.Magenta, + PathRadius = 1f + } + }, + Position = new Vector2(100) + }, + new Container + { + Children = + new Drawable[] + { + convertedDrawablePath = new SmoothPath(), + convertedControlPointDrawablePath = new SmoothPath + { + Colour = Colour4.Magenta, + PathRadius = 1f + } + }, + Position = new Vector2(100, 300) + } + }; + + resetPath(); + } + + [SetUp] + public void Setup() => Schedule(resetPath); + + private void resetPath() + { + path = new SliderPath(); + convertedPath = new SliderPath(); + + path.Version.ValueChanged += getConvertedControlPoints; + } + + private void getConvertedControlPoints(ValueChangedEvent obj) + { + convertedPath.ControlPoints.Clear(); + convertedPath.ControlPoints.AddRange(BezierConverter.ConvertToModernBezier(path.ControlPoints)); + } + + protected override void Update() + { + base.Update(); + + List vertices = new List(); + + path.GetPathToProgress(vertices, 0, 1); + + drawablePath.Vertices = vertices; + controlPointDrawablePath.Vertices = path.ControlPoints.Select(o => o.Position).ToList(); + + if (controlPointDrawablePath.Vertices.Count > 0) + { + controlPointDrawablePath.Position = + drawablePath.PositionInBoundingBox(drawablePath.Vertices[0]) - controlPointDrawablePath.PositionInBoundingBox(controlPointDrawablePath.Vertices[0]); + } + + vertices.Clear(); + + convertedPath.GetPathToProgress(vertices, 0, 1); + + convertedDrawablePath.Vertices = vertices; + convertedControlPointDrawablePath.Vertices = convertedPath.ControlPoints.Select(o => o.Position).ToList(); + + if (convertedControlPointDrawablePath.Vertices.Count > 0) + { + convertedControlPointDrawablePath.Position = convertedDrawablePath.PositionInBoundingBox(convertedDrawablePath.Vertices[0]) + - convertedControlPointDrawablePath.PositionInBoundingBox(convertedControlPointDrawablePath.Vertices[0]); + } + } + + [Test] + public void TestEmptyPath() + { + } + + [TestCase(PathType.Linear)] + [TestCase(PathType.Bezier)] + [TestCase(PathType.Catmull)] + [TestCase(PathType.PerfectCurve)] + public void TestSingleSegment(PathType type) + => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + + [TestCase(PathType.Linear)] + [TestCase(PathType.Bezier)] + [TestCase(PathType.Catmull)] + [TestCase(PathType.PerfectCurve)] + public void TestMultipleSegment(PathType type) + { + AddStep("create path", () => + { + path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); + }); + } + + [Test] + public void TestComplex() + { + AddStep("create path", () => + { + path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero)); + }); + } + + [TestCase(0, 100)] + [TestCase(1, 100)] + [TestCase(5, 100)] + [TestCase(10, 100)] + [TestCase(30, 100)] + [TestCase(50, 100)] + [TestCase(100, 100)] + [TestCase(100, 1)] + public void TestPerfectCurveAngles(float height, float width) + { + AddStep("create path", () => + { + path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0))); + }); + } + + [TestCase(2)] + [TestCase(4)] + public void TestPerfectCurveFallbackScenarios(int points) + { + AddStep("create path", () => + { + switch (points) + { + case 2: + path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100))); + break; + + case 4: + path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + break; + } + }); + } + + private List createSegment(PathType type, params Vector2[] controlPoints) + { + var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList(); + points[0].Type = type; + return points; + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index bdf8cc5136..24969414d0 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,11 +1,11 @@  - + - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index bdef46a6b2..9f2a088a4b 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -6,7 +6,7 @@ - + WinExe diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs new file mode 100644 index 0000000000..ebee36a7db --- /dev/null +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -0,0 +1,287 @@ +// 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 System.Linq; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Objects +{ + public static class BezierConverter + { + private struct CircleBezierPreset + { + public readonly double ArcLength; + public readonly Vector2d[] ControlPoints; + + public CircleBezierPreset(double arcLength, Vector2d[] controlPoints) + { + ArcLength = arcLength; + ControlPoints = controlPoints; + } + } + + // Extremely accurate a bezier anchor positions for approximating circles of several arc lengths + private static readonly CircleBezierPreset[] circle_presets = + { + new CircleBezierPreset(0.4993379862754501, + new[] { new Vector2d(1, 0), new Vector2d(1, 0.2549893626632736f), new Vector2d(0.8778997558480327f, 0.47884446188920726f) }), + new CircleBezierPreset(1.7579419829169447, + new[] { new Vector2d(1, 0), new Vector2d(1, 0.6263026f), new Vector2d(0.42931178f, 1.0990661f), new Vector2d(-0.18605515f, 0.9825393f) }), + new CircleBezierPreset(3.1385246920140215, + new[] { new Vector2d(1, 0), new Vector2d(1, 0.87084764f), new Vector2d(0.002304826f, 1.5033062f), new Vector2d(-0.9973236f, 0.8739115f), new Vector2d(-0.9999953f, 0.0030679568f) }), + new CircleBezierPreset(5.69720464620727, + new[] { new Vector2d(1, 0), new Vector2d(1, 1.4137783f), new Vector2d(-1.4305235f, 2.0779421f), new Vector2d(-2.3410065f, -0.94017583f), new Vector2d(0.05132711f, -1.7309346f), new Vector2d(0.8331702f, -0.5530167f) }), + new CircleBezierPreset(2 * Math.PI, + new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) }) + }; + + /// + /// Converts a slider path to bezier control point positions compatible with the legacy osu! client. + /// + /// The control points of the path. + /// The offset for the whole path. + /// The list of legacy bezier control point positions. + public static List ConvertToLegacyBezier(IList controlPoints, Vector2 position) + { + Vector2[] vertices = new Vector2[controlPoints.Count]; + for (int i = 0; i < controlPoints.Count; i++) + vertices[i] = controlPoints[i].Position; + + var result = new List(); + int start = 0; + + for (int i = 0; i < controlPoints.Count; i++) + { + if (controlPoints[i].Type == null && i < controlPoints.Count - 1) + continue; + + // The current vertex ends the segment + var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); + var segmentType = controlPoints[start].Type ?? PathType.Linear; + + switch (segmentType) + { + case PathType.Catmull: + result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position); + + break; + + case PathType.Linear: + result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position); + + break; + + case PathType.PerfectCurve: + result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position)); + + break; + + default: + foreach (Vector2 v in segmentVertices) + { + result.Add(v + position); + } + + break; + } + + // Start the new segment at the current vertex + start = i; + } + + return result; + } + + /// + /// Converts a path of control points to an identical path using only Bezier type control points. + /// + /// The control points of the path. + /// The list of bezier control points. + public static List ConvertToModernBezier(IList controlPoints) + { + Vector2[] vertices = new Vector2[controlPoints.Count]; + for (int i = 0; i < controlPoints.Count; i++) + vertices[i] = controlPoints[i].Position; + + var result = new List(); + int start = 0; + + for (int i = 0; i < controlPoints.Count; i++) + { + if (controlPoints[i].Type == null && i < controlPoints.Count - 1) + continue; + + // The current vertex ends the segment + var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); + var segmentType = controlPoints[start].Type ?? PathType.Linear; + + switch (segmentType) + { + case PathType.Catmull: + foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices)) + { + for (int j = 0; j < segment.Length - 1; j++) + { + result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null)); + } + } + + break; + + case PathType.Linear: + foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices)) + { + for (int j = 0; j < segment.Length - 1; j++) + { + result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null)); + } + } + + break; + + case PathType.PerfectCurve: + var circleResult = ConvertCircleToBezierAnchors(segmentVertices); + + for (int j = 0; j < circleResult.Length - 1; j++) + { + result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null)); + } + + break; + + default: + for (int j = 0; j < segmentVertices.Length - 1; j++) + { + result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null)); + } + + break; + } + + // Start the new segment at the current vertex + start = i; + } + + result.Add(new PathControlPoint(controlPoints[^1].Position)); + + return result; + } + + /// + /// Converts perfect curve anchors to bezier anchors. + /// + /// The control point positions to convert. + public static Vector2[] ConvertCircleToBezierAnchors(ReadOnlySpan controlPoints) + { + if (controlPoints.Length != 3) + return controlPoints.ToArray(); + + var pr = new CircularArcProperties(controlPoints); + if (!pr.IsValid) + return controlPoints.ToArray(); + + CircleBezierPreset preset = circle_presets.Last(); + + foreach (CircleBezierPreset cbp in circle_presets) + { + if (cbp.ArcLength < pr.ThetaRange) continue; + + preset = cbp; + break; + } + + double arcLength = preset.ArcLength; + var arc = new Vector2d[preset.ControlPoints.Length]; + preset.ControlPoints.CopyTo(arc, 0); + + // Converge on arcLength of thetaRange + int n = arc.Length - 1; + double tf = pr.ThetaRange / arcLength; + + while (Math.Abs(tf - 1) > 1E-7) + { + for (int j = 0; j < n; j++) + { + for (int i = n; i > j; i--) + { + arc[i] = arc[i] * tf + arc[i - 1] * (1 - tf); + } + } + + arcLength = Math.Atan2(arc.Last()[1], arc.Last()[0]); + + if (arcLength < 0) + { + arcLength += 2 * Math.PI; + } + + tf = pr.ThetaRange / arcLength; + } + + // Adjust rotation, radius, and position + var result = new Vector2[arc.Length]; + + for (int i = 0; i < arc.Length; i++) + { + result[i] = new Vector2( + (float)((Math.Cos(pr.ThetaStart) * arc[i].X + -Math.Sin(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.X), + (float)((Math.Sin(pr.ThetaStart) * arc[i].X + Math.Cos(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.Y)); + } + + return result; + } + + /// + /// Converts catmull anchors to bezier anchors. + /// + /// The control point positions to convert. + public static Vector2[][] ConvertCatmullToBezierAnchors(ReadOnlySpan controlPoints) + { + int iLen = controlPoints.Length; + var bezier = new Vector2[iLen - 1][]; + + for (int i = 0; i < iLen - 1; i++) + { + var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i]; + var v2 = controlPoints[i]; + var v3 = i < iLen - 1 ? controlPoints[i + 1] : v2 + v2 - v1; + var v4 = i < iLen - 2 ? controlPoints[i + 2] : v3 + v3 - v2; + + bezier[i] = new[] + { + v2, + (-v1 + 6 * v2 + v3) / 6, + (-v4 + 6 * v3 + v2) / 6, + v3 + }; + } + + return bezier; + } + + /// + /// Converts linear anchors to bezier anchors. + /// + /// The control point positions to convert. + public static Vector2[][] ConvertLinearToBezierAnchors(ReadOnlySpan controlPoints) + { + int iLen = controlPoints.Length; + var bezier = new Vector2[iLen - 1][]; + + for (int i = 0; i < iLen - 1; i++) + { + bezier[i] = new[] + { + controlPoints[i], + controlPoints[i + 1] + }; + } + + return bezier; + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fe44ed3688..9a6866d264 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,10 +23,10 @@ - - - - + + + + @@ -34,10 +34,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - +