public override RibbonParameters GetHoldRibbonParameters(RuntimeNote startNote, RuntimeNote endNote, float now, NoteMetrics noteMetrics, NoteAnimationMetrics animationMetrics)
        {
            var tp1 = NoteAnimationHelper.CalculateNoteTimePoints(startNote, animationMetrics);
            var tp2 = NoteAnimationHelper.CalculateNoteTimePoints(endNote, animationMetrics);

            var t1    = GetTransformedTime(startNote, now, tp1);
            var t2    = GetTransformedTime(endNote, now, tp2);
            var tperc = startNote.IsHold() ? 0.5 : 0.4;
            var tm    = MathHelperEx.Lerp(t1, t2, tperc);

            var x1 = GetNoteX(startNote, now, noteMetrics, animationMetrics);
            var x2 = GetNoteX(endNote, now, noteMetrics, animationMetrics);

            var   startStatus = NoteAnimationHelper.GetOnStageStatusOf(startNote, now, tp1);
            float y1;

            if (startStatus == OnStageStatus.Passed)
            {
                y1 = animationMetrics.Bottom;
            }
            else
            {
                y1 = GetNoteOnStageY(t1, animationMetrics);
            }

            var   endStatus = NoteAnimationHelper.GetOnStageStatusOf(endNote, now, tp2);
            float y2;

            if (endStatus == OnStageStatus.Incoming)
            {
                y2 = animationMetrics.Top;
            }
            else
            {
                y2 = GetNoteOnStageY(t2, animationMetrics);
            }

            // CGSS-like
            //var xm = GetNoteOnStageX(endNote.StartX, endNote.EndX, tm, animationMetrics);
            // Naive guess
            //var xm = (x1 + x2) / 2;
            var xm = MathHelperEx.Lerp(x1, x2, 0.5f);

            if (startNote.IsSlide())
            {
                if (endNote.EndX < startNote.EndX)
                {
                    xm -= animationMetrics.Width * 0.02f * ((tp2.Leave - now) / (tp2.Leave - tp1.Enter));
                }
                else if (endNote.EndX > startNote.EndX)
                {
                    xm += animationMetrics.Width * 0.02f * ((tp2.Leave - now) / (tp2.Leave - tp1.Enter));
                }
            }
            var ym = GetNoteOnStageY(tm, animationMetrics);

            var(cx1, cx2) = GetBezierFromQuadratic(x1, xm, x2);
            var(cy1, cy2) = GetBezierFromQuadratic(y1, ym, y2);
            return(new RibbonParameters(x1, y1, cx1, cy1, cx2, cy2, x2, y2));
        }
        public override RibbonParameters GetSlideRibbonParameters(RuntimeNote startNote, RuntimeNote endNote, float now, NoteMetrics noteMetrics, NoteAnimationMetrics animationMetrics)
        {
            var tp1        = NoteAnimationHelper.CalculateNoteTimePoints(startNote, animationMetrics);
            var thisStatus = NoteAnimationHelper.GetOnStageStatusOf(startNote, now, tp1);

            if (thisStatus < OnStageStatus.Passed)
            {
                return(GetHoldRibbonParameters(startNote, endNote, now, noteMetrics, animationMetrics));
            }

            var tp2 = NoteAnimationHelper.CalculateNoteTimePoints(endNote, animationMetrics);

            var t1    = GetTransformedTime(startNote, now, tp1);
            var t2    = GetTransformedTime(endNote, now, tp2);
            var tperc = startNote.IsHold() ? 0.5 : 0.4;
            var tm    = MathHelperEx.Lerp(t1, t2, tperc);

            var trackCount       = animationMetrics.TrackCount;
            var leftMarginRatio  = animationMetrics.NoteEndXRatios[0];
            var rightMarginRatio = animationMetrics.NoteEndXRatios[trackCount - 1];
            var startXRatio      = leftMarginRatio + (rightMarginRatio - leftMarginRatio) * (startNote.EndX / (trackCount - 1));
            var endXRatio        = leftMarginRatio + (rightMarginRatio - leftMarginRatio) * (endNote.EndX / (trackCount - 1));

            var perc    = (now - startNote.HitTime) / (endNote.HitTime - startNote.HitTime);
            var x1Ratio = MathHelperEx.Lerp(startXRatio, endXRatio, perc);

            var x1 = animationMetrics.Width * x1Ratio;
            var y1 = animationMetrics.Bottom;
            var x2 = GetNoteX(endNote, now, noteMetrics, animationMetrics);
            var y2 = GetNoteOnStageY(t2, animationMetrics);

            // CGSS-like
            //var xm = GetNoteOnStageX(endNote.StartX, endNote.EndX, tm, animationMetrics);
            // Naive guess
            //var xm = (x1 + x2) / 2;
            var xm = MathHelperEx.Lerp(x1, x2, 0.5f);

            if (startNote.IsSlide())
            {
                if (endNote.EndX < startNote.EndX)
                {
                    xm -= animationMetrics.Width * 0.02f * ((tp2.Leave - now) / (tp2.Leave - tp1.Enter));
                }
                else if (endNote.EndX > startNote.EndX)
                {
                    xm += animationMetrics.Width * 0.02f * ((tp2.Leave - now) / (tp2.Leave - tp1.Enter));
                }
            }
            var ym = GetNoteOnStageY(tm, animationMetrics);

            var(cx1, cx2) = GetBezierFromQuadratic(x1, xm, x2);
            var(cy1, cy2) = GetBezierFromQuadratic(y1, ym, y2);
            return(new RibbonParameters(x1, y1, cx1, cy1, cx2, cy2, x2, y2));
        }