1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 17:07:38 +08:00

Merge pull request #2107 from smoogipoo/sliderbody-rework

Give DrawableSlider an accurate position and size
This commit is contained in:
Dean Herbert 2018-02-26 16:03:47 +09:00 committed by GitHub
commit b9b5d00096
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 101 additions and 64 deletions

View File

@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
StartTime = lastTickTime, StartTime = lastTickTime,
ComboColour = ComboColour, ComboColour = ComboColour,
X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, X = X + Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
{ {
Bank = s.Bank, Bank = s.Bank,
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
StartTime = spanStartTime + t, StartTime = spanStartTime + t,
ComboColour = ComboColour, ComboColour = ComboColour,
X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, X = X + Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
{ {
Bank = s.Bank, Bank = s.Bank,
@ -120,14 +120,14 @@ namespace osu.Game.Rulesets.Catch.Objects
Samples = Samples, Samples = Samples,
ComboColour = ComboColour, ComboColour = ComboColour,
StartTime = spanStartTime + spanDuration, StartTime = spanStartTime + spanDuration,
X = Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
}); });
} }
} }
public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;
public float EndX => Curve.PositionAt(this.ProgressAt(1)).X / CatchPlayfield.BASE_WIDTH; public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
public double Duration => EndTime - StartTime; public double Duration => EndTime - StartTime;

View File

@ -78,8 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0; bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0;
List<Vector2> curve = drawableSlider.Body.CurrentCurve; List<Vector2> curve = drawableSlider.Body.CurrentCurve;
var positionOnCurve = isRepeatAtEnd ? end : start; Position = isRepeatAtEnd ? end : start;
Position = positionOnCurve + drawableSlider.HitObject.StackOffset;
if (curve.Count < 2) if (curve.Count < 2)
return; return;
@ -90,10 +89,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// find the next vector2 in the curve which is not equal to our current position to infer a rotation. // find the next vector2 in the curve which is not equal to our current position to infer a rotation.
for (int i = searchStart; i >= 0 && i < curve.Count; i += direction) for (int i = searchStart; i >= 0 && i < curve.Count; i += direction)
{ {
if (curve[i] == positionOnCurve) if (curve[i] == Position)
continue; continue;
Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - positionOnCurve.Y, curve[i].X - positionOnCurve.X)); Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - Position.Y, curve[i].X - Position.X));
break; break;
} }
} }

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
slider = s; slider = s;
Position = s.StackedPosition;
DrawableSliderTail tail; DrawableSliderTail tail;
Container<DrawableSliderTick> ticks; Container<DrawableSliderTick> ticks;
Container<DrawableRepeatPoint> repeatPoints; Container<DrawableRepeatPoint> repeatPoints;
@ -39,20 +42,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Body = new SliderBody(s) Body = new SliderBody(s)
{ {
AccentColour = AccentColour, AccentColour = AccentColour,
Position = s.StackedPosition,
PathWidth = s.Scale * 64, PathWidth = s.Scale * 64,
}, },
ticks = new Container<DrawableSliderTick>(), ticks = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatPoints = new Container<DrawableRepeatPoint>(), repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s) Ball = new SliderBall(s)
{ {
BypassAutoSizeAxes = Axes.Both,
Scale = new Vector2(s.Scale), Scale = new Vector2(s.Scale),
AccentColour = AccentColour, AccentColour = AccentColour,
AlwaysPresent = true, AlwaysPresent = true,
Alpha = 0 Alpha = 0
}, },
HeadCircle = new DrawableHitCircle(s.HeadCircle), HeadCircle = new DrawableHitCircle(s.HeadCircle) { Position = s.HeadCircle.Position - s.Position },
tail = new DrawableSliderTail(s.TailCircle) tail = new DrawableSliderTail(s.TailCircle) { Position = s.TailCircle.Position - s.Position }
}; };
components.Add(Body); components.Add(Body);
@ -65,10 +68,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var tick in s.NestedHitObjects.OfType<SliderTick>()) foreach (var tick in s.NestedHitObjects.OfType<SliderTick>())
{ {
var drawableTick = new DrawableSliderTick(tick) var drawableTick = new DrawableSliderTick(tick) { Position = tick.Position - s.Position };
{
Position = tick.StackedPosition
};
ticks.Add(drawableTick); ticks.Add(drawableTick);
components.Add(drawableTick); components.Add(drawableTick);
@ -77,10 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var repeatPoint in s.NestedHitObjects.OfType<RepeatPoint>()) foreach (var repeatPoint in s.NestedHitObjects.OfType<RepeatPoint>())
{ {
var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) { Position = repeatPoint.Position - s.Position };
{
Position = repeatPoint.StackedPosition
};
repeatPoints.Add(drawableRepeatPoint); repeatPoints.Add(drawableRepeatPoint);
components.Add(drawableRepeatPoint); components.Add(drawableRepeatPoint);
@ -107,11 +104,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!HeadCircle.IsHit) if (!HeadCircle.IsHit)
HeadCircle.Position = slider.StackedPositionAt(completionProgress); HeadCircle.Position = slider.CurvePositionAt(completionProgress);
foreach (var c in components.OfType<ISliderProgress>()) c.UpdateProgress(completionProgress); foreach (var c in components.OfType<ISliderProgress>()) c.UpdateProgress(completionProgress);
foreach (var c in components.OfType<ITrackSnaking>()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0)); foreach (var c in components.OfType<ITrackSnaking>()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0));
foreach (var t in components.OfType<IRequireTracking>()) t.Tracking = Ball.Tracking; foreach (var t in components.OfType<IRequireTracking>()) t.Tracking = Ball.Tracking;
Size = Body.Size;
OriginPosition = Body.PathOffset;
if (DrawSize.X > 0 && DrawSize.Y > 0)
{
var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize);
foreach (var obj in NestedHitObjects)
obj.RelativeAnchorPosition = childAnchorPosition;
Ball.RelativeAnchorPosition = childAnchorPosition;
}
} }
protected override void CheckForJudgements(bool userTriggered, double timeOffset) protected override void CheckForJudgements(bool userTriggered, double timeOffset)

View File

@ -19,8 +19,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSliderTail(HitCircle hitCircle) public DrawableSliderTail(HitCircle hitCircle)
: base(hitCircle) : base(hitCircle)
{ {
AlwaysPresent = true; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
AlwaysPresent = true;
} }
protected override void CheckForJudgements(bool userTriggered, double timeOffset) protected override void CheckForJudgements(bool userTriggered, double timeOffset)

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Rulesets.Objects.Types;
using OpenTK; using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public void UpdateProgress(double completionProgress) public void UpdateProgress(double completionProgress)
{ {
Position = slider.StackedPositionAt(completionProgress); Position = slider.CurvePositionAt(completionProgress);
} }
} }
} }

View File

@ -29,6 +29,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
set { path.PathWidth = value; } set { path.PathWidth = value; }
} }
/// <summary>
/// Offset in absolute coordinates from the start of the curve.
/// </summary>
public Vector2 PathOffset { get; private set; }
public readonly List<Vector2> CurrentCurve = new List<Vector2>();
public readonly Bindable<bool> SnakingIn = new Bindable<bool>(); public readonly Bindable<bool> SnakingIn = new Bindable<bool>();
public readonly Bindable<bool> SnakingOut = new Bindable<bool>(); public readonly Bindable<bool> SnakingOut = new Bindable<bool>();
@ -75,6 +82,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private int textureWidth => (int)PathWidth * 2; private int textureWidth => (int)PathWidth * 2;
private Vector2 topLeftOffset;
private readonly Slider slider; private readonly Slider slider;
public SliderBody(Slider s) public SliderBody(Slider s)
{ {
@ -84,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
container = new BufferedContainer container = new BufferedContainer
{ {
RelativeSizeAxes = Axes.Both,
CacheDrawnFrameBuffer = true, CacheDrawnFrameBuffer = true,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -107,11 +117,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
if (updateSnaking(p0, p1)) if (updateSnaking(p0, p1))
{ {
// Autosizing does not give us the desired behaviour here. // The path is generated such that its size encloses it. This change of size causes the path
// We want the container to have the same size as the slider, // to move around while snaking, so we need to offset it to make sure it maintains the
// and to be positioned such that the slider head is at (0,0). // same position as when it is fully snaked.
container.Size = path.Size; var newTopLeftOffset = path.PositionInBoundingBox(Vector2.Zero);
container.Position = -path.PositionInBoundingBox(slider.Curve.PositionAt(0) - CurrentCurve[0]); path.Position = topLeftOffset - newTopLeftOffset;
container.ForceRedraw(); container.ForceRedraw();
} }
@ -121,6 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private void load() private void load()
{ {
reloadTexture(); reloadTexture();
computeSize();
} }
private void reloadTexture() private void reloadTexture()
@ -164,7 +175,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
path.Texture = texture; path.Texture = texture;
} }
public readonly List<Vector2> CurrentCurve = new List<Vector2>(); private void computeSize()
{
// Generate the entire curve
slider.Curve.GetPathToProgress(CurrentCurve, 0, 1);
foreach (Vector2 p in CurrentCurve)
path.AddVertex(p);
Size = path.Size;
topLeftOffset = path.PositionInBoundingBox(Vector2.Zero);
PathOffset = path.PositionInBoundingBox(CurrentCurve[0]);
}
private bool updateSnaking(double p0, double p1) private bool updateSnaking(double p0, double p1)
{ {
if (SnakedStart == p0 && SnakedEnd == p1) return false; if (SnakedStart == p0 && SnakedEnd == p1) return false;
@ -176,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
path.ClearVertices(); path.ClearVertices();
foreach (Vector2 p in CurrentCurve) foreach (Vector2 p in CurrentCurve)
path.AddVertex(p - CurrentCurve[0]); path.AddVertex(p);
return true; return true;
} }

View File

@ -23,8 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects
public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;
public double Duration => EndTime - StartTime; public double Duration => EndTime - StartTime;
public Vector2 StackedPositionAt(double t) => this.PositionAt(t) + StackOffset; public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
public override Vector2 EndPosition => this.PositionAt(1); public override Vector2 EndPosition => Position + this.CurvePositionAt(1);
public SliderCurve Curve { get; } = new SliderCurve(); public SliderCurve Curve { get; } = new SliderCurve();
@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Objects
HeadCircle = new HitCircle HeadCircle = new HitCircle
{ {
StartTime = StartTime, StartTime = StartTime,
Position = StackedPosition, Position = Position,
IndexInCurrentCombo = IndexInCurrentCombo, IndexInCurrentCombo = IndexInCurrentCombo,
ComboColour = ComboColour, ComboColour = ComboColour,
Samples = Samples, Samples = Samples,
@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TailCircle = new HitCircle TailCircle = new HitCircle
{ {
StartTime = EndTime, StartTime = EndTime,
Position = StackedEndPosition, Position = EndPosition,
IndexInCurrentCombo = IndexInCurrentCombo, IndexInCurrentCombo = IndexInCurrentCombo,
ComboColour = ComboColour ComboColour = ComboColour
}; };
@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Objects
SpanIndex = span, SpanIndex = span,
SpanStartTime = spanStartTime, SpanStartTime = spanStartTime,
StartTime = spanStartTime + timeProgress * SpanDuration, StartTime = spanStartTime + timeProgress * SpanDuration,
Position = Curve.PositionAt(distanceProgress), Position = Position + Curve.PositionAt(distanceProgress),
StackHeight = StackHeight, StackHeight = StackHeight,
Scale = Scale, Scale = Scale,
ComboColour = ComboColour, ComboColour = ComboColour,
@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Objects
RepeatIndex = repeatIndex, RepeatIndex = repeatIndex,
SpanDuration = SpanDuration, SpanDuration = SpanDuration,
StartTime = StartTime + repeat * SpanDuration, StartTime = StartTime + repeat * SpanDuration,
Position = Curve.PositionAt(repeat % 2), Position = Position + Curve.PositionAt(repeat % 2),
StackHeight = StackHeight, StackHeight = StackHeight,
Scale = Scale, Scale = Scale,
ComboColour = ComboColour, ComboColour = ComboColour,

View File

@ -118,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Tests
ComboColour = Color4.LightSeaGreen, ComboColour = Color4.LightSeaGreen,
ControlPoints = new List<Vector2> ControlPoints = new List<Vector2>
{ {
new Vector2(-(distance / 2), 0), Vector2.Zero,
new Vector2(distance / 2, 0), new Vector2(distance, 0),
}, },
Distance = distance, Distance = distance,
RepeatCount = repeats, RepeatCount = repeats,
@ -139,9 +139,9 @@ namespace osu.Game.Rulesets.Osu.Tests
ComboColour = Color4.LightSeaGreen, ComboColour = Color4.LightSeaGreen,
ControlPoints = new List<Vector2> ControlPoints = new List<Vector2>
{ {
new Vector2(-200, 0), Vector2.Zero,
new Vector2(0, 200), new Vector2(200, 200),
new Vector2(200, 0) new Vector2(400, 0)
}, },
Distance = 600, Distance = 600,
RepeatCount = repeats, RepeatCount = repeats,
@ -163,12 +163,12 @@ namespace osu.Game.Rulesets.Osu.Tests
ComboColour = Color4.LightSeaGreen, ComboColour = Color4.LightSeaGreen,
ControlPoints = new List<Vector2> ControlPoints = new List<Vector2>
{ {
new Vector2(-200, 0), Vector2.Zero,
new Vector2(-50, 75), new Vector2(150, 75),
new Vector2(0, 100),
new Vector2(100, -200),
new Vector2(200, 0), new Vector2(200, 0),
new Vector2(230, 0) new Vector2(300, -200),
new Vector2(400, 0),
new Vector2(430, 0)
}, },
Distance = 793.4417, Distance = 793.4417,
RepeatCount = repeats, RepeatCount = repeats,
@ -190,11 +190,11 @@ namespace osu.Game.Rulesets.Osu.Tests
ComboColour = Color4.LightSeaGreen, ComboColour = Color4.LightSeaGreen,
ControlPoints = new List<Vector2> ControlPoints = new List<Vector2>
{ {
new Vector2(-200, 0), Vector2.Zero,
new Vector2(-50, 75), new Vector2(150, 75),
new Vector2(0, 100), new Vector2(200, 100),
new Vector2(100, -200), new Vector2(300, -200),
new Vector2(230, 0) new Vector2(430, 0)
}, },
Distance = 480, Distance = 480,
RepeatCount = repeats, RepeatCount = repeats,
@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.Tests
ComboColour = Color4.LightSeaGreen, ComboColour = Color4.LightSeaGreen,
ControlPoints = new List<Vector2> ControlPoints = new List<Vector2>
{ {
new Vector2(0, 0), Vector2.Zero,
new Vector2(-200, 0), new Vector2(-200, 0),
new Vector2(0, 0), new Vector2(0, 0),
new Vector2(0, -200), new Vector2(0, -200),
@ -247,10 +247,10 @@ namespace osu.Game.Rulesets.Osu.Tests
CurveType = CurveType.Catmull, CurveType = CurveType.Catmull,
ControlPoints = new List<Vector2> ControlPoints = new List<Vector2>
{ {
new Vector2(-100, 0), Vector2.Zero,
new Vector2(-50, -50), new Vector2(50, -50),
new Vector2(50, 50), new Vector2(150, 50),
new Vector2(100, 0) new Vector2(200, 0)
}, },
Distance = 300, Distance = 300,
RepeatCount = repeats, RepeatCount = repeats,

View File

@ -35,13 +35,13 @@ namespace osu.Game.Tests.Visual
new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }, new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f },
new Slider new Slider
{ {
Position = new Vector2(128, 256),
ControlPoints = new List<Vector2> ControlPoints = new List<Vector2>
{ {
new Vector2(128, 256), Vector2.Zero,
new Vector2(344, 256), new Vector2(216, 0),
}, },
Distance = 400, Distance = 400,
Position = new Vector2(128, 256),
Velocity = 1, Velocity = 1,
TickDistance = 100, TickDistance = 100,
Scale = 0.5f, Scale = 0.5f,

View File

@ -41,9 +41,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
} }
else if ((type & ConvertHitObjectType.Slider) > 0) else if ((type & ConvertHitObjectType.Slider) > 0)
{ {
var pos = new Vector2(int.Parse(split[0]), int.Parse(split[1]));
CurveType curveType = CurveType.Catmull; CurveType curveType = CurveType.Catmull;
double length = 0; double length = 0;
var points = new List<Vector2> { new Vector2(int.Parse(split[0]), int.Parse(split[1])) }; var points = new List<Vector2> { Vector2.Zero };
string[] pointsplit = split[5].Split('|'); string[] pointsplit = split[5].Split('|');
foreach (string t in pointsplit) foreach (string t in pointsplit)
@ -69,7 +71,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
} }
string[] temp = t.Split(':'); string[] temp = t.Split(':');
points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture))); points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos);
} }
int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture); int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture);
@ -134,7 +136,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++) for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
result = CreateSlider(new Vector2(int.Parse(split[0]), int.Parse(split[1])), combo, points, length, curveType, repeatCount, nodeSamples); result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples);
} }
else if ((type & ConvertHitObjectType.Spinner) > 0) else if ((type & ConvertHitObjectType.Spinner) > 0)
{ {

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Types
/// <param name="obj">The curve.</param> /// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param> /// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
/// <returns>The position on the curve.</returns> /// <returns>The position on the curve.</returns>
public static Vector2 PositionAt(this IHasCurve obj, double progress) public static Vector2 CurvePositionAt(this IHasCurve obj, double progress)
=> obj.Curve.PositionAt(obj.ProgressAt(progress)); => obj.Curve.PositionAt(obj.ProgressAt(progress));
/// <summary> /// <summary>