For anyone else looking for how to do this, I found Graphs and Charts and used that code with a small refactor.
I made an empty VisualElement with the correct x/y size called “circle-holder”, and then did VisualElement.Add(new CooldownCircle()) into it. This has been working great.
public class CooldownCircle : VisualElement {
private float _fraction;
public CooldownCircle() {
generateVisualContent += OnGenerateVisualContent;
}
public void SetCooldownRemaining(float fraction) {
_fraction = fraction;
}
void OnGenerateVisualContent(MeshGenerationContext mgc) {
Rect rect = contentRect;
// TODO: It's not clear why height/y is always 0, even after waiting for resolvedStyle, but width/x seems to be correct.
Vector2 center = new(rect.center.x, rect.center.x);
// Fill 100% of the square not just a circle.
float radius = rect.size.x * 1.5f / 2;
// Vertical up is -PI/2. Ending at 3*PI/2 is the full circle.
float startAngle = -Mathf.PI / 2.0f + (1 - _fraction) * 2 * Mathf.PI;
CircleCreator.Circle(center, radius, startAngle, 3*Mathf.PI/2.0f, GameColors.CooldownCircle, mgc);
}
}
public static class CircleCreator {
public static readonly int NumSegments = 250;
public static void Circle(Vector2 pos, float radius, float startAngle, float endAngle, Color color, MeshGenerationContext context) {
var segments = NumSegments;
var mesh = context.Allocate(segments + 1, (segments - 1) * 3);
mesh.SetNextVertex(new Vertex() { position = new Vector2(pos.x, pos.y), tint = color });
var angle = startAngle;
var range = endAngle - startAngle;
var step = range / (segments - 1);
for (int i = 0; i < segments; i++) {
Vector2 offset = new(radius * Mathf.Cos(angle), radius * Mathf.Sin(angle));
mesh.SetNextVertex(new Vertex() { position = new Vector2(pos.x, pos.y) + offset, tint = color });
angle += step;
}
for (ushort i = 1; i < segments; i++) {
mesh.SetNextIndex(0);
mesh.SetNextIndex(i);
mesh.SetNextIndex((ushort)(i + 1));
}
}
}