// 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 JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Types; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { /// /// Visualises the s between two s. /// public class FollowPointGroup : CompositeDrawable { // Todo: These shouldn't be constants private const int spacing = 32; private const double preempt = 800; /// /// The start time of . /// public readonly Bindable StartTime = new Bindable(); /// /// The which s will exit from. /// [NotNull] public readonly DrawableOsuHitObject Start; /// /// Creates a new . /// /// The which s will exit from. public FollowPointGroup([NotNull] DrawableOsuHitObject start) { Start = start; RelativeSizeAxes = Axes.Both; StartTime.BindTo(Start.HitObject.StartTimeBindable); } protected override void LoadComplete() { base.LoadComplete(); bindEvents(Start); } private DrawableOsuHitObject end; /// /// The which s will enter. /// [CanBeNull] public DrawableOsuHitObject End { get => end; set { end = value; if (end != null) bindEvents(end); if (IsLoaded) scheduleRefresh(); else refresh(); } } private void bindEvents(DrawableOsuHitObject drawableObject) { drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh()); drawableObject.HitObject.DefaultsApplied += scheduleRefresh; } private void scheduleRefresh() => Scheduler.AddOnce(refresh); private void refresh() { ClearInternal(); if (End == null) return; OsuHitObject osuStart = Start.HitObject; OsuHitObject osuEnd = End.HitObject; if (osuEnd.NewCombo) return; if (osuStart is Spinner || osuEnd is Spinner) return; Vector2 startPosition = osuStart.EndPosition; Vector2 endPosition = osuEnd.Position; double startTime = (osuStart as IHasEndTime)?.EndTime ?? osuStart.StartTime; double endTime = osuEnd.StartTime; Vector2 distanceVector = endPosition - startPosition; int distance = (int)distanceVector.Length; float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); double duration = endTime - startTime; for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) { float fraction = (float)d / distance; Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector; Vector2 pointEndPosition = startPosition + fraction * distanceVector; double fadeOutTime = startTime + fraction * duration; double fadeInTime = fadeOutTime - preempt; FollowPoint fp; AddInternal(fp = new FollowPoint { Position = pointStartPosition, Rotation = rotation, Alpha = 0, Scale = new Vector2(1.5f * osuEnd.Scale), }); using (fp.BeginAbsoluteSequence(fadeInTime)) { fp.FadeIn(osuEnd.TimeFadeIn); fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out); fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out); fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn); } fp.Expire(true); } } } }