// 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.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning { /// /// A which changes its curve depending on the snaking progress. /// public abstract partial class SnakingSliderBody : SliderBody, ISliderProgress { public readonly List CurrentCurve = new List(); public readonly Bindable SnakingIn = new Bindable(); public readonly Bindable SnakingOut = new Bindable(); public double? SnakedStart { get; private set; } public double? SnakedEnd { get; private set; } public override float PathRadius { get => base.PathRadius; set { if (base.PathRadius == value) return; base.PathRadius = value; Refresh(); } } public override Vector2 PathOffset => snakedPathOffset; public override Vector2 PathEndOffset => snakedPathEndOffset; /// /// The top-left position of the path when fully snaked. /// private Vector2 snakedPosition; /// /// The offset of the path from when fully snaked. /// private Vector2 snakedPathOffset; /// /// The offset of the end of path from when fully snaked. /// private Vector2 snakedPathEndOffset; private DrawableSlider drawableSlider = null!; [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { drawableSlider = (DrawableSlider)drawableObject; Refresh(); } public void UpdateProgress(double completionProgress) { if (drawableSlider.HitObject == null) return; Slider slider = drawableSlider.HitObject; int span = slider.SpanAt(completionProgress); double spanProgress = slider.ProgressAt(completionProgress); double start = 0; double end = SnakingIn.Value ? Math.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1; if (span >= slider.SpanCount() - 1) { if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1) { start = 0; end = SnakingOut.Value ? spanProgress : 1; } else { start = SnakingOut.Value ? spanProgress : 0; } } setRange(start, end); } public void Refresh() { if (drawableSlider.HitObject == null) return; // Generate the entire curve drawableSlider.HitObject.Path.GetPathToProgress(CurrentCurve, 0, 1); SetVertices(CurrentCurve); // Force the body to be the final path size to avoid excessive autosize computations Path.AutoSizeAxes = Axes.Both; Size = Path.Size; updatePathSize(); snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]); double lastSnakedStart = SnakedStart ?? 0; double lastSnakedEnd = SnakedEnd ?? 0; SnakedStart = null; SnakedEnd = null; setRange(lastSnakedStart, lastSnakedEnd); } public override void RecyclePath() { base.RecyclePath(); updatePathSize(); } private void updatePathSize() { // Force the path to its final size to avoid excessive framebuffer resizes Path.AutoSizeAxes = Axes.None; Path.Size = Size; } private void setRange(double p0, double p1) { if (p0 > p1) (p0, p1) = (p1, p0); if (SnakedStart == p0 && SnakedEnd == p1) return; SnakedStart = p0; SnakedEnd = p1; drawableSlider.HitObject.Path.GetPathToProgress(CurrentCurve, p0, p1); SetVertices(CurrentCurve); // The bounding box of the path expands as it snakes, which in turn shifts the position of the path. // Depending on the direction of expansion, it may appear as if the path is expanding towards the position of the slider // rather than expanding out from the position of the slider. // To remove this effect, the path's position is shifted towards its final snaked position Path.Position = snakedPosition - Path.PositionInBoundingBox(Vector2.Zero); } } }