There's almost certainly a cleaner way to write this, but I wasn't able to find a fully-formed solution that returns a proper collision manifold for contact resolution anywhere on the web. Just a bunch of half-baked stuff. So I wrote and tested this. I think it works correctly for all cases, but I was especially concerned with the case where the circle is fully inside of the AABB for my use-case.
Red = circle that is colliding
Green = new position of circle after contact resolution
Blue Dot = point-of-contact
Orange Line = contact resolution vector (normal * depth)

struct Rectangle { float x, y, width, height; };
bool dlb_CheckCollisionCircleRec(Vector2 center, float radius, Rectangle rec, Vector2 &contact, Vector2 &normal, float &depth)
{
float xOverlap = 0;
float yOverlap = 0;
Vector2 pt = center; // circle center point constrained to box
if (pt.x < rec.x) {
pt.x = rec.x;
} else if (pt.x >= rec.x + rec.width) {
pt.x = rec.x + rec.width;
} else {
float recCenterX = rec.x + rec.width / 2;
xOverlap = pt.x < recCenterX ? rec.x - pt.x : (rec.x + rec.width) - pt.x;
}
if (pt.y < rec.y) {
pt.y = rec.y;
} else if (pt.y >= rec.y + rec.height) {
pt.y = rec.y + rec.height;
} else {
float recCenterY = rec.y + rec.height / 2;
yOverlap = pt.y < recCenterY ? rec.y - pt.y : (rec.y + rec.height) - pt.y;
}
if (Vector2DistanceSqr(pt, center) < radius*radius) {
contact = pt; // closest point to "pt" on the surface of the box
if (fabsf(xOverlap) < fabsf(yOverlap)) {
contact.x += xOverlap;
} else if (fabsf(yOverlap) < fabsf(xOverlap)) {
contact.y += yOverlap;
}
Vector2 pen = Vector2Subtract(center, contact);
depth = radius - Vector2Length(pen);
normal = Vector2Normalize(pen);
if (xOverlap && yOverlap) {
depth -= radius*2;
}
return true;
}
return false;
}
This is how I use it in the code. Note that the dot product check is optional, and is there to allow circles with outward velocities to escape the rectangle as opposed to getting stuck inside of it.
struct Vector2 { float x, y; };
float Vector2DotProduct(Vector2 v1, Vector2 v2) {
return v1.x*v2.x + v1.y*v2.y;
}
Vector2 contact{};
Vector2 normal{};
float depth{};
if (dlb_CheckCollisionCircleRec(entity.position, entity.radius, tileRect, contact, normal, depth)) {
entity.colliding = true;
if (Vector2DotProduct(entity.velocity, normal) < 0) {
entity.position.x += normal.x * depth;
entity.position.y += normal.y * depth;
}
}