A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using osu.Framework.Graphics.Textures;
5using osuTK.Graphics.ES30;
6using osuTK;
7using System;
8using osu.Framework.Graphics.Batches;
9using osu.Framework.Graphics.Primitives;
10using osuTK.Graphics;
11using osu.Framework.Extensions.MatrixExtensions;
12using osu.Framework.Graphics.OpenGL.Vertices;
13
14namespace osu.Framework.Graphics.UserInterface
15{
16 public class CircularProgressDrawNode : TexturedShaderDrawNode
17 {
18 private const float arc_tolerance = 0.1f;
19
20 private const float two_pi = MathF.PI * 2;
21
22 protected new CircularProgress Source => (CircularProgress)base.Source;
23
24 private LinearBatch<TexturedVertex2D> halfCircleBatch;
25
26 private float angle;
27 private float innerRadius = 1;
28
29 private Vector2 drawSize;
30 private Texture texture;
31
32 public CircularProgressDrawNode(CircularProgress source)
33 : base(source)
34 {
35 }
36
37 public override void ApplyState()
38 {
39 base.ApplyState();
40
41 texture = Source.Texture;
42 drawSize = Source.DrawSize;
43 angle = (float)Source.Current.Value * two_pi;
44 innerRadius = Source.InnerRadius;
45 }
46
47 private Vector2 pointOnCircle(float angle) => new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
48 private float angleToUnitInterval(float angle) => angle / two_pi + (angle >= 0 ? 0 : 1);
49
50 // Gets colour at the localPos position in the unit square of our Colour gradient box.
51 private Color4 colourAt(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
52 ? DrawColourInfo.Colour.TopLeft.Linear
53 : DrawColourInfo.Colour.Interpolate(localPos).Linear;
54
55 private static readonly Vector2 origin = new Vector2(0.5f, 0.5f);
56
57 private void updateVertexBuffer()
58 {
59 const float start_angle = 0;
60
61 float dir = Math.Sign(angle);
62 float radius = Math.Max(drawSize.X, drawSize.Y);
63
64 // The amount of points are selected such that discrete curvature is smaller than the provided tolerance.
65 // The exact angle required to meet the tolerance is: 2 * Math.Acos(1 - TOLERANCE / r)
66 // The special case is for extremely small circles where the radius is smaller than the tolerance.
67 int amountPoints = 2 * radius <= arc_tolerance ? 2 : Math.Max(2, (int)Math.Ceiling(Math.PI / Math.Acos(1 - arc_tolerance / radius)));
68
69 if (halfCircleBatch == null || halfCircleBatch.Size < amountPoints * 2)
70 {
71 halfCircleBatch?.Dispose();
72
73 // Amount of points is multiplied by 2 to account for each part requiring two vertices.
74 halfCircleBatch = new LinearBatch<TexturedVertex2D>(amountPoints * 2, 1, PrimitiveType.TriangleStrip);
75 }
76
77 Matrix3 transformationMatrix = DrawInfo.Matrix;
78 MatrixExtensions.ScaleFromLeft(ref transformationMatrix, drawSize);
79
80 Vector2 current = origin + pointOnCircle(start_angle) * 0.5f;
81 Color4 currentColour = colourAt(current);
82 current = Vector2Extensions.Transform(current, transformationMatrix);
83
84 Vector2 screenOrigin = Vector2Extensions.Transform(origin, transformationMatrix);
85 Color4 originColour = colourAt(origin);
86
87 // Offset by 0.5 pixels inwards to ensure we never sample texels outside the bounds
88 RectangleF texRect = texture.GetTextureRect(new RectangleF(0.5f, 0.5f, texture.Width - 1, texture.Height - 1));
89
90 float prevOffset = dir >= 0 ? 0 : 1;
91
92 // First center point
93 halfCircleBatch.Add(new TexturedVertex2D
94 {
95 Position = Vector2.Lerp(current, screenOrigin, innerRadius),
96 TexturePosition = new Vector2(dir >= 0 ? texRect.Left : texRect.Right, texRect.Top),
97 Colour = originColour
98 });
99
100 // First outer point.
101 halfCircleBatch.Add(new TexturedVertex2D
102 {
103 Position = new Vector2(current.X, current.Y),
104 TexturePosition = new Vector2(dir >= 0 ? texRect.Left : texRect.Right, texRect.Bottom),
105 Colour = currentColour
106 });
107
108 for (int i = 1; i < amountPoints; i++)
109 {
110 float fract = (float)i / (amountPoints - 1);
111
112 // Clamps the angle so we don't overshoot.
113 // dir is used so negative angles result in negative angularOffset.
114 float angularOffset = Math.Min(fract * two_pi, dir * angle);
115 float normalisedOffset = angularOffset / two_pi;
116
117 if (dir < 0)
118 normalisedOffset += 1.0f;
119
120 // Update `current`
121 current = origin + pointOnCircle(start_angle + angularOffset) * 0.5f;
122 currentColour = colourAt(current);
123 current = Vector2Extensions.Transform(current, transformationMatrix);
124
125 // current center point
126 halfCircleBatch.Add(new TexturedVertex2D
127 {
128 Position = Vector2.Lerp(current, screenOrigin, innerRadius),
129 TexturePosition = new Vector2(texRect.Left + (normalisedOffset + prevOffset) / 2 * texRect.Width, texRect.Top),
130 Colour = originColour
131 });
132
133 // current outer point
134 halfCircleBatch.Add(new TexturedVertex2D
135 {
136 Position = new Vector2(current.X, current.Y),
137 TexturePosition = new Vector2(texRect.Left + normalisedOffset * texRect.Width, texRect.Bottom),
138 Colour = currentColour
139 });
140
141 prevOffset = normalisedOffset;
142 }
143 }
144
145 public override void Draw(Action<TexturedVertex2D> vertexAction)
146 {
147 base.Draw(vertexAction);
148
149 if (texture?.Available != true)
150 return;
151
152 Shader.Bind();
153
154 texture.TextureGL.Bind();
155
156 updateVertexBuffer();
157
158 Shader.Unbind();
159 }
160
161 protected override void Dispose(bool isDisposing)
162 {
163 base.Dispose(isDisposing);
164
165 halfCircleBatch?.Dispose();
166 }
167 }
168}