Disclaimer: This method requires you to have access to EditableImages, which requires ID verification.
Module to create circular progress bars. Uses an EditableImage.
It is not a Roblox package or sorts because it is just a few lines of code. Just copy it away.
Note that this forces rounded caps.
Source code
--!strict
local vec2 = Vector2.new
local cos, sin, floor, ceil, sqrt, atan2, pi, add = math.cos, math.sin, math.floor, math.ceil, math.sqrt, math.atan2, math.pi, table.insert
local function is_between(a: number, b: number, c: number)
if b <= c then
return a >= b and a <= c
else
return a >= b or a <= c
end
end
type props = {
-- main bar
source: EditableImage;
center: Vector2?;
radius: number;
thickness: number;
a_start: number;
a_end: number;
color: Color3?;
alpha: number?;
-- border
border_thickness: number?;
border_color: Color3?;
border_alpha: number?;
}
type propx = {
source: EditableImage;
center: Vector2;
radius: number;
thickness: number;
a_start: number;
a_end: number;
color: Color3;
alpha: number;
}
local function draw(props: propx)
local source = props.source
local center = props.center or props.source.Size/2
local thickness = props.thickness or 1
local color = props.color or Color3.new(1,1,1)
-- endcaps
local cap_r = thickness / 2 -- radius
local cap_pos_1 = center + vec2(cos(props.a_start), sin(props.a_start)) * props.radius -- positions
local cap_pos_2 = center + vec2(cos(props.a_end), sin(props.a_end)) * props.radius
-- inner and outer radius of the bar
local ir = props.radius - cap_r
local xr = props.radius + cap_r
-- limits for scanline
local min_y = floor(center.Y - xr)
local max_y = ceil(center.Y + xr)
for y = min_y, max_y do
local spans = {}
local in_span = false
local span_str
local min_x = floor(center.X - xr)
local max_x = ceil(center.X + xr)
for x = min_x, max_x do
local dx, dy = x - center.X, y - center.Y
local dist = sqrt(dx*dx + dy*dy)
local ok =
dist >= ir
and dist <= xr
and is_between(
(atan2(dy, dx) + 2 * pi) % (2 * pi), props.a_start, props.a_end
)
if ok then
if not in_span then
in_span = true
span_str = x
end
elseif in_span then
in_span = false
add(spans, {span_str, x-1})
end
end
if in_span then
add(spans, {span_str, max_x})
end
for _, span: {number} in spans do
local x0, x1 = span[1], span[2]
local width = x1 - x0 + 1
local buf = buffer.create(width * 4)
local idx = 0
for i = 1, width do
buffer.writeu8(buf, idx, floor(color.R * 255))
buffer.writeu8(buf, idx+1, floor(color.G * 255))
buffer.writeu8(buf, idx+2, floor(color.B * 255))
buffer.writeu8(buf, idx+3, floor(((1 - props.alpha) * 255)))
idx += 4
end
source:WritePixelsBuffer(vec2(x0, y), vec2(width, 1), buf)
end
end
-- draw caps
source:DrawCircle(cap_pos_1, cap_r, color, 0, Enum.ImageCombineType.Overwrite)
source:DrawCircle(cap_pos_2, cap_r, color, 0, Enum.ImageCombineType.Overwrite)
end
return function(props: props)
if typeof(props.source) ~= 'Object' then
error('did not provide a valid EditableImage')
end
if (type(props.a_start) ~= 'number') and (type(props.a_end) ~= 'number') then
error('invalid angles provided')
end
if type(props.radius) ~= 'number' or props.radius <= 0 then
error('invalid radius provided')
end
if type(props.thickness) ~= 'number' or props.thickness <= 0 then
error('invalid thickness provided')
end
-- normalize angles to the top instead of right
local a_start = (math.rad(props.a_start) - (pi / 2) + 2 * pi) % (2 * pi)
local a_end = (math.rad(props.a_end) - (pi / 2) + 2 * pi) % (2 * pi)
local source = props.source
local center = props.center or source.Size / 2
local thickness = props.thickness or 1
local radius = props.radius
local color = props.color or Color3.new(1,1,1)
local alpha = props.alpha or 0;
local border_thickness = props.border_thickness or 0
local border_color = props.border_color or Color3.new()
local border_alpha = props.border_alpha or 0
if border_thickness > 0 then
draw {
source = source;
center = center;
radius = radius;
thickness = border_thickness * 2 + thickness;
a_start = a_start;
a_end = a_end;
color = border_color;
alpha = border_alpha
}
end
draw {
source = source;
center = center;
radius = radius;
thickness = thickness;
a_start = a_start;
a_end = a_end;
color = color;
alpha = alpha
}
end
How to use
It is but a simple function that takes a table
with the following keys:
Property | Type | Required | Additional Notes |
---|---|---|---|
source | EditableImage | true |
|
center | Vector2 | false |
Defaults to center if not provided |
radius | number | true |
|
thickness | number | true |
|
a_start | number | true |
Takes degrees (0-360). 0 is image top |
a_end | number | true |
Takes degrees (0-360) |
color | Color3 | false |
Defaults to white |
alpha | number | false |
0-1, opaque-transparent |
border_thickness | number | false |
If not provided won’t render |
border_color | Color3 | false |
Defaults to black |
border_alpha | number | false |
0-1 |
And some examples.
Example 1: Simple Arc
Code:
local draw = require(...)
local img = whatever_way_to_get_an_Editable_Image_Size_500
draw {
source = img;
radius = 120;
thickness = 60;
a_start = 270;
a_end = 120;
color = Color3.new(0.439216, 1, 0.552941);
alpha = 0;
border_thickness = 10;
border_color = Color3.new()
}
Result:
Example 2: Sectioned 'Analytics' Bar
Code:
local draw = require(...)
local img = EditableImageWithSize512
draw {
source = img,
radius = 256 - 48,
thickness = 48,
a_start = 20,
a_end = 250,
color = Color3.new(0.27451, 0.854902, 0.439216),
alpha = 0,
border_thickness = 0,
}
draw {
source = img,
radius = 256 - 48,
thickness = 48,
a_start = 270,
a_end = 340,
color = Color3.new(0.854902, 0.262745, 0.270588),
alpha = 0,
border_thickness = 0,
}
draw {
source = img,
radius = 256 - 48,
thickness = 48,
a_start = 0,
a_end = 0,
color = Color3.new(0.839216, 0.839216, 0.839216),
alpha = 0,
border_thickness = 0,
}
Wanted to create a circular loading bar but my brain isn’t working at the moment. Anyway have fun.
How does it perform?
- Simple. Smaller size = faster rendering. For a 512px image, it renders in around 50ms. Which is slow. Quite slow actually. Example 2 takes around ~130ms. Can it be faster? Probably. I’ll figure it out.