Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs
2264 views
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osuTK;

namespace osu.Game.Screens.SelectV2
{
    public partial class BeatmapMetadataWedge
    {
        private partial class FailRetryDisplay : CompositeDrawable
        {
            private readonly GraphDrawable retriesGraph;
            private readonly GraphDrawable failsGraph;

            public APIFailTimes Data
            {
                set
                {
                    int[] retries = value.Retries ?? Array.Empty<int>();
                    int[] fails = value.Fails ?? Array.Empty<int>();
                    int[] total = retries.Zip(fails, (r, f) => r + f).ToArray();

                    int maximum = total.DefaultIfEmpty(0).Max();

                    retriesGraph.Data = total.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
                    failsGraph.Data = fails.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray();
                }
            }

            public FailRetryDisplay()
            {
                RelativeSizeAxes = Axes.X;
                AutoSizeAxes = Axes.Y;

                InternalChild = new FillFlowContainer
                {
                    RelativeSizeAxes = Axes.X,
                    AutoSizeAxes = Axes.Y,
                    Direction = FillDirection.Vertical,
                    Spacing = new Vector2(0f, 4f),
                    Children = new Drawable[]
                    {
                        new OsuSpriteText
                        {
                            Text = BeatmapsetsStrings.ShowInfoPointsOfFailure,
                            Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
                            Margin = new MarginPadding { Bottom = 4f },
                        },
                        new Container
                        {
                            RelativeSizeAxes = Axes.X,
                            Height = 65f,
                            Children = new[]
                            {
                                retriesGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both, Y = -1f },
                                failsGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both },
                            },
                        },
                    },
                };
            }

            [BackgroundDependencyLoader]
            private void load(OsuColour colours)
            {
                retriesGraph.Colour = colours.Orange1;
                failsGraph.Colour = colours.DarkOrange2;
            }

            private partial class GraphDrawable : Drawable
            {
                private readonly float[] displayedData = new float[100];

                private float[] data = new float[100];

                public float[] Data
                {
                    get => data;
                    set
                    {
                        data = value;
                        Invalidate(Invalidation.DrawNode);
                    }
                }

                protected override void Update()
                {
                    base.Update();

                    bool changed = false;

                    for (int i = 0; i < displayedData.Length; i++)
                    {
                        float before = displayedData[i];
                        float value = data.ElementAtOrDefault(i);
                        displayedData[i] = (float)Interpolation.DampContinuously(displayedData[i], value, 40, Time.Elapsed);
                        changed |= displayedData[i] != before;
                    }

                    if (changed)
                        Invalidate(Invalidation.DrawNode);
                }

                protected override DrawNode CreateDrawNode() => new GraphDrawNode(this);

                // todo: consider integrating this with BarGraph
                // this is different from BarGraph since this displays each bar with corner radii applied.
                private class GraphDrawNode : DrawNode
                {
                    private readonly GraphDrawable source;

                    private Vector2 drawSize;
                    private float[] displayedData = null!;

                    public GraphDrawNode(GraphDrawable source)
                        : base(source)
                    {
                        this.source = source;
                    }

                    public override void ApplyState()
                    {
                        base.ApplyState();

                        drawSize = source.DrawSize;
                        displayedData = source.displayedData;
                    }

                    protected override void Draw(IRenderer renderer)
                    {
                        base.Draw(renderer);

                        const float spacing_constant = 1.5f;

                        float position = 0;
                        float barWidth = drawSize.X / displayedData.Length / spacing_constant;

                        float totalSpacing = drawSize.X - barWidth * displayedData.Length;
                        float spacing = totalSpacing / (displayedData.Length - 1);

                        for (int i = 0; i < displayedData.Length; i++)
                        {
                            float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth);

                            drawBar(renderer, position, barWidth, barHeight);

                            position += barWidth + spacing;
                        }
                    }

                    private void drawBar(IRenderer renderer, float position, float width, float height)
                    {
                        float cornerRadius = width / 2f;

                        Vector3 scale = DrawInfo.MatrixInverse.ExtractScale();
                        float blendRange = (scale.X + scale.Y) / 2;

                        RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height));
                        Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix;

                        renderer.PushMaskingInfo(new MaskingInfo
                        {
                            ScreenSpaceAABB = screenSpaceDrawQuad.AABB,
                            MaskingRect = drawRectangle.Normalize(),
                            ConservativeScreenSpaceQuad = screenSpaceDrawQuad,
                            ToMaskingSpace = DrawInfo.MatrixInverse,
                            CornerRadius = cornerRadius,
                            CornerExponent = 2f,
                            // We are setting the linear blend range to the approximate size of a _pixel_ here.
                            // This results in the optimal trade-off between crispness and smoothness of the
                            // edges of the masked region according to sampling theory.
                            BlendRange = blendRange,
                            AlphaExponent = 1,
                        });

                        renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour);
                        renderer.PopMaskingInfo();
                    }
                }
            }
        }
    }
}