A game framework written with osu! in mind.
at master 168 lines 6.5 kB view raw
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}