// 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 System.Linq; using System.Threading; using osu.Framework; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Allocation; using osu.Framework.Threading; namespace osu.Game.Screens.Play { public class SquareGraph : Container { private BufferedContainer columns; public int ColumnCount => columns?.Children.Count ?? 0; private int progress; public int Progress { get => progress; set { if (value == progress) return; progress = value; redrawProgress(); } } private float[] calculatedValues = Array.Empty(); // values but adjusted to fit the amount of columns private int[] values; public int[] Values { get => values; set { if (value == values) return; values = value; layout.Invalidate(); } } private Color4 fillColour; public Color4 FillColour { get => fillColour; set { if (value == fillColour) return; fillColour = value; redrawFilled(); } } public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) { if ((invalidation & Invalidation.DrawSize) > 0) layout.Invalidate(); return base.Invalidate(invalidation, source, shallPropagate); } private readonly Cached layout = new Cached(); private ScheduledDelegate scheduledCreate; protected override void Update() { base.Update(); if (values != null && !layout.IsValid) { columns?.FadeOut(500, Easing.OutQuint).Expire(); scheduledCreate?.Cancel(); scheduledCreate = Scheduler.AddDelayed(RecreateGraph, 500); layout.Validate(); } } private CancellationTokenSource cts; /// /// Recreates the entire graph. /// protected virtual void RecreateGraph() { var newColumns = new BufferedContainer { CacheDrawnFrameBuffer = true, RedrawOnScale = false, RelativeSizeAxes = Axes.Both, }; for (float x = 0; x < DrawWidth; x += Column.WIDTH) { newColumns.Add(new Column(DrawHeight) { LitColour = fillColour, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Position = new Vector2(x, 0), State = ColumnState.Dimmed, }); } cts?.Cancel(); LoadComponentAsync(newColumns, c => { Child = columns = c; columns.FadeInFromZero(500, Easing.OutQuint); recalculateValues(); redrawFilled(); redrawProgress(); }, (cts = new CancellationTokenSource()).Token); } /// /// Redraws all the columns to match their lit/dimmed state. /// private void redrawProgress() { for (int i = 0; i < ColumnCount; i++) columns[i].State = i <= progress ? ColumnState.Lit : ColumnState.Dimmed; columns?.ForceRedraw(); } /// /// Redraws the filled amount of all the columns. /// private void redrawFilled() { for (int i = 0; i < ColumnCount; i++) columns[i].Filled = calculatedValues.ElementAtOrDefault(i); columns?.ForceRedraw(); } /// /// Takes and adjusts it to fit the amount of columns. /// private void recalculateValues() { var newValues = new List(); if (values == null) { for (float i = 0; i < ColumnCount; i++) newValues.Add(0); return; } var max = values.Max(); float step = values.Length / (float)ColumnCount; for (float i = 0; i < values.Length; i += step) { newValues.Add((float)values[(int)i] / max); } calculatedValues = newValues.ToArray(); } public class Column : Container, IStateful { protected readonly Color4 EmptyColour = Color4.White.Opacity(20); public Color4 LitColour = Color4.LightBlue; protected readonly Color4 DimmedColour = Color4.White.Opacity(140); private float cubeCount => DrawHeight / WIDTH; private const float cube_size = 4; private const float padding = 2; public const float WIDTH = cube_size + padding; public event Action StateChanged; private readonly List drawableRows = new List(); private float filled; public float Filled { get => filled; set { if (value == filled) return; filled = value; fillActive(); } } private ColumnState state; public ColumnState State { get => state; set { if (value == state) return; state = value; if (IsLoaded) fillActive(); StateChanged?.Invoke(State); } } public Column(float height) { Width = WIDTH; Height = height; } [BackgroundDependencyLoader] private void load() { drawableRows.AddRange(Enumerable.Range(0, (int)cubeCount).Select(r => new Box { Size = new Vector2(cube_size), Position = new Vector2(0, r * WIDTH + padding), })); Children = drawableRows; // Reverse drawableRows so when iterating through them they start at the bottom drawableRows.Reverse(); } protected override void LoadComplete() { base.LoadComplete(); fillActive(); } private void fillActive() { Color4 colour = State == ColumnState.Lit ? LitColour : DimmedColour; int countFilled = (int)Math.Clamp(filled * drawableRows.Count, 0, drawableRows.Count); for (int i = 0; i < drawableRows.Count; i++) drawableRows[i].Colour = i < countFilled ? colour : EmptyColour; } } public enum ColumnState { Lit, Dimmed } } }