The SUNC Drawing API
The Drawing library lets scripts paint primitives directly onto the screen, outside Roblox's own UI stack. It renders above everything else, doesn't cost an instance, and is the backbone of features like ESPs, FPS counters, and debug overlays. This article is a complete reference for every object type and every shared property in the SUNC spec.
The big picture
Every drawing object is created with Drawing.new(type), which returns an object with properties you can read and write like a table. Setting Visible = true makes the object render every frame; setting Visible = false hides it without destroying it. When you're done with an object you call :Remove() on it — the underlying handle is released and the object becomes unusable.
local line = Drawing.new("Line")
line.From = Vector2.new(100, 100)
line.To = Vector2.new(300, 300)
line.Color = Color3.fromRGB(255, 0, 0)
line.Thickness = 2
line.Visible = true
-- ...later
line:Remove()DrawingVisible is true.Shared properties
Every drawing object shares a base set of properties:
Visible: boolean— render this frame or skip it. Defaults tofalse.ZIndex: number— relative draw order. Higher renders on top.Transparency: number— 0 = fully transparent, 1 = fully opaque. Yes, the convention is flipped from Roblox's usualTransparencyproperty.Color: Color3— the primary colour of the primitive. What "primary" means varies by type.
Transparency is opacity (1 = fully visible). Roblox UI's Transparency is the opposite. This is the single most common Drawing bug — a value of 0 makes your drawing invisible, not fully visible.Line
The simplest type. Draws a straight line between two screen points.
local line = Drawing.new("Line")
line.From = Vector2.new(0, 0) -- starting screen position
line.To = Vector2.new(800, 600) -- ending screen position
line.Thickness = 2 -- line width in pixels
line.Color = Color3.fromRGB(255, 0, 0)
line.Visible = trueType-specific properties: From: Vector2, To: Vector2, Thickness: number.
Text
Renders a string at a position with control over font, size, and centering.
local text = Drawing.new("Text")
text.Text = "FPS: 60"
text.Position = Vector2.new(20, 20)
text.Size = 18
text.Center = false -- if true, Position is the centre point
text.Outline = true -- draw a 1px outline for contrast
text.OutlineColor = Color3.fromRGB(0, 0, 0)
text.Color = Color3.fromRGB(255, 255, 255)
text.Font = Drawing.Fonts.UI -- enum: UI | System | Plex | Monospace
text.Visible = trueType-specific properties: Text: string, Position: Vector2, Size: number, Center: boolean, Outline: boolean, OutlineColor: Color3, Font: number. The Font values come from theDrawing.Fonts enum:
Drawing.Fonts = {
UI = 0,
System = 1,
Plex = 2,
Monospace = 3,
}A useful Text-only property is TextBounds — read- only, returns the rendered pixel size as a Vector2. Handy when you need to lay out adjacent elements without guessing widths.
Image
Renders an image from raw byte data. You supply the bytes; the drawing handles decoding and rendering.
local img = Drawing.new("Image")
img.Data = game:HttpGet("https://example.com/icon.png")
img.Position = Vector2.new(50, 50)
img.Size = Vector2.new(64, 64)
img.Transparency = 1
img.Visible = trueType-specific properties: Data: string (the raw binary bytes of a PNG or JPEG), Size: Vector2, Position: Vector2, Rounding: number (corner radius in pixels).
Circle
Renders a circle. Can be filled or stroked.
local circle = Drawing.new("Circle")
circle.Position = Vector2.new(200, 200)
circle.Radius = 50
circle.NumSides = 32 -- approximation; 32 looks smooth
circle.Thickness = 2
circle.Filled = false -- true = fill, false = stroke only
circle.Color = Color3.fromRGB(0, 255, 100)
circle.Visible = trueType-specific properties: Position: Vector2, Radius: number, NumSides: number, Thickness: number, Filled: boolean.
Square
Axis-aligned rectangle. Position is the top-left corner; Size is the width/height.
local box = Drawing.new("Square")
box.Position = Vector2.new(100, 100)
box.Size = Vector2.new(200, 80)
box.Thickness = 2
box.Filled = false
box.Color = Color3.fromRGB(255, 200, 0)
box.Visible = trueType-specific properties: Position: Vector2, Size: Vector2, Thickness: number, Filled: boolean.
Quad
Four arbitrary corners. Used for non-axis-aligned shapes — rotated boxes, perspective markers, etc.
local quad = Drawing.new("Quad")
quad.PointA = Vector2.new(100, 100)
quad.PointB = Vector2.new(220, 80)
quad.PointC = Vector2.new(240, 200)
quad.PointD = Vector2.new(120, 220)
quad.Thickness = 2
quad.Filled = false
quad.Color = Color3.fromRGB(120, 180, 255)
quad.Visible = trueType-specific properties: PointA / B / C / D: Vector2, Thickness, Filled. Points are traversed in declaration order to form the outline.
Triangle
Three corners; otherwise identical in shape semantics to Quad.
local tri = Drawing.new("Triangle")
tri.PointA = Vector2.new(150, 50)
tri.PointB = Vector2.new(250, 200)
tri.PointC = Vector2.new(50, 200)
tri.Thickness = 2
tri.Filled = true
tri.Color = Color3.fromRGB(220, 80, 220)
tri.Visible = trueType-specific properties: PointA / B / C: Vector2, Thickness, Filled.
A small ESP example
Putting it together: a per-player box ESP. Each player gets a square that follows their character's screen position, plus a Text element above it with their name. The whole thing is three drawings updated per frame.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local camera = workspace.CurrentCamera
local function createEsp(player)
local box = Drawing.new("Square")
box.Thickness = 1
box.Filled = false
box.Color = Color3.fromRGB(0, 255, 0)
box.Visible = false
local label = Drawing.new("Text")
label.Size = 14
label.Center = true
label.Outline = true
label.Color = Color3.fromRGB(255, 255, 255)
label.Visible = false
RunService.RenderStepped:Connect(function()
local char = player.Character
local head = char and char:FindFirstChild("Head")
if not head then
box.Visible = false
label.Visible = false
return
end
local pos, onScreen = camera:WorldToViewportPoint(head.Position)
if not onScreen then
box.Visible = false
label.Visible = false
return
end
local height = 60
local width = 32
box.Size = Vector2.new(width, height)
box.Position = Vector2.new(pos.X - width / 2, pos.Y - 20)
label.Text = player.Name
label.Position = Vector2.new(pos.X, pos.Y - 36)
box.Visible = true
label.Visible = true
end)
end
for _, p in Players:GetPlayers() do
if p ~= Players.LocalPlayer then createEsp(p) end
endTwo things to notice. First, the connection runs every render step — drawings don't reposition themselves, you reposition them. Second, off-screen handling: when the world point falls outside the camera, we hide both drawings instead of letting them snap to the nearest edge. That detail is what separates a rough ESP from a clean one.
Wrap-up
The Drawing library is small and shaped exactly like its name — seven primitive types, a handful of shared properties per type, and one global function to instantiate them. Most non-trivial script overlays you'll see in the wild are built on these eight or nine pieces, occasionally combined with Roblox UI where pointer interaction is needed. (Drawings don't capture input — that's ScreenGuis' job.)
Ather is the lead developer behind Atherhub. He's been writing Luau and Roblox tooling for the better part of a decade, with a focus on the messy interface between game-script internals and the platforms that host them. Have feedback on this article? Drop it in the Discord.