Skip to content

Drawing

Call aqua.draw functions inside on_draw(). A drawing lasts one frame, so draw it again every frame.

Positions are screen pixels from the top-left corner. Colors are { r, g, b, a } from 0 to 1; alpha is optional. Options go in a table at the end.

local WHITE = { 1, 1, 1 }
local PANEL = { 0.05, 0.05, 0.08, 0.9 }
function on_draw()
aqua.draw.rect(20, 20, 180, 36, PANEL, { rounding = 6 })
aqua.draw.text(32, 28, "Hello from Lua", WHITE, { size = 16 })
end

You can also pass a color as separate numbers (1, 0, 0, 1). If a number follows it, include the alpha, or that number is read as the alpha.

Function Draws Options
rect(x, y, w, h, color, [opts]) Filled rectangle rounding, clip
rect_outline(x, y, w, h, color, [opts]) Rectangle border rounding, thickness, clip
rect_gradient(x, y, w, h, tl, tr, br, bl, [opts]) Rectangle blending four corner colors rounding, clip
line(x1, y1, x2, y2, color, [opts]) Line thickness, clip
circle(x, y, radius, color, [opts]) Circle outline thickness, segments, clip
arc(x, y, radius, startDeg, endDeg, color, [opts]) Part of a circle outline thickness, segments, clip
polyline(points, color, [opts]) Line through a list of points thickness, closed, clip
bar(x, y, w, h, fraction, background, fill, [opts]) Bar filled from 0 to 1 rounding, vertical, clip
Option Meaning
rounding Corner radius in pixels. Default 0
thickness Line width in pixels. Default 1
segments Straight pieces per curve. Default 48
closed Join the last point back to the first
vertical Fill from the bottom up
clip { x, y, w, h }: draw only inside this rectangle

A number instead of the table sets thickness (rounding for rect). Arc angles are degrees: 0 points right and 90 points down.

local WHITE = { 1, 1, 1 }
local RED = { 1, 0.2, 0.2 }
local GREEN = { 0.2, 0.9, 0.3 }
local DARK = { 0.1, 0.1, 0.1, 0.9 }
local BLUE = { 0.3, 0.55, 1, 1 }
local CLEAR = { 0.3, 0.55, 1, 0 }
function on_draw()
aqua.draw.rect(20, 20, 120, 60, DARK, { rounding = 4 })
aqua.draw.rect_outline(20, 100, 120, 60, WHITE, { thickness = 2 })
aqua.draw.line(20, 180, 140, 220, RED, 2)
-- Centre (80, 280), radius 40
aqua.draw.circle(80, 280, 40, WHITE, 2)
-- Top half: from pointing left (180) to pointing right (360)
aqua.draw.arc(80, 380, 40, 180, 360, WHITE, 2)
-- Points as { x, y } pairs, or a flat list: { 10, 400, 50, 470, 90, 410 }
aqua.draw.polyline({ { 10, 400 }, { 50, 470 }, { 90, 410 } }, RED, { closed = true })
-- 75% full
aqua.draw.bar(20, 500, 120, 6, 0.75, DARK, GREEN)
-- Corners: top-left, top-right, bottom-right, bottom-left
aqua.draw.rect_gradient(20, 520, 200, 40, BLUE, CLEAR, CLEAR, BLUE)
end
Function Does
text(x, y, str, color, [opts]) Draws text. x, y is the top-left of the line
text_width(str, [size], [font]) Width in pixels
text_metrics([size], [font]) lineHeight, ascent, sizePx
Option Meaning
size Pixels. Snaps to the nearest available size from 10 to 32
align "left", "center" or "right"; x becomes that point of the text
shadow true for a drop shadow
outline true for a black outline, or a color
font Handle from load_font
clip { x, y, w, h }: draw only inside this rectangle
local WHITE = { 1, 1, 1 }
function on_draw()
local screenW = aqua.draw.screen_size()
aqua.draw.text(20, 20, "hello", WHITE, { size = 14 })
aqua.draw.text(screenW / 2, 40, "centered", WHITE, { size = 18, align = "center", outline = true })
end

To fit a box around text, measure the height with text_metrics, not size: a line is taller than its font size.

local SIZE, PAD = 14, 6
function on_draw()
local w = aqua.draw.text_width("aqua!v2", SIZE)
local h = aqua.draw.text_metrics(SIZE)
aqua.draw.rect(20, 20, w + PAD * 2, h + PAD * 2, { 0, 0, 0, 0.8 }, 4)
aqua.draw.text(20 + PAD, 20 + PAD, "aqua!v2", { 1, 1, 1 }, { size = SIZE })
end

load_font(path) loads a .ttf or .otf from your script folder and returns a handle, or nil if the file is missing or not a font.

local font = aqua.draw.load_font("Roboto-Medium.ttf") -- once, outside on_draw
function on_draw()
aqua.draw.text(20, 20, "aqua!v2", { 1, 1, 1 }, { size = 13, font = font })
end
  • Pass the same handle to text_width to measure with that font.
  • load_font(path, { hinting = "none" }) gives softer letters that keep their original shape.
  • Characters the font lacks are drawn with the default font.
Function Returns
world_to_screen(point) or (x, y, z) sx, sy, visible
world_to_screen_many(points) { x, y, visible } per point
screen_size() width, height

visible is false for points behind the camera.

local WHITE = { 1, 1, 1 }
function on_draw()
for _, player in ipairs(aqua.get_players()) do
if player.alive and player.origin then
local x, y, visible = aqua.draw.world_to_screen(player.origin)
if visible then
aqua.draw.text(x, y, player.name, WHITE, { size = 12, align = "center" })
end
end
end
end

Skeleton:

local ORANGE = { 1, 0.5, 0.2, 0.9 }
local edges = aqua.skeleton.edges() -- pairs of bone numbers to connect
function on_draw()
for _, player in ipairs(aqua.get_players()) do
if player.bones then
local screen = aqua.draw.world_to_screen_many(player.bones)
for _, edge in ipairs(edges) do
local a, b = screen[edge[1]], screen[edge[2]]
if a and b and a.visible and b.visible then
aqua.draw.line(a.x, a.y, b.x, b.y, ORANGE, 1.5)
end
end
end
end
end
Function Draws
icon(x, y, w, h, name, [color], [opts]) Flat weapon icon, as in ESP: "ak47" or a def index
weapon(x, y, w, h, defIndex, [paintId | opts]) Inventory picture of a weapon
econ(x, y, w, h, key, [color], [opts]) Inventory picture by key
image(x, y, w, h, path, [color], [opts]) PNG from your script folder (png is the same function)

Inventory pictures load in the background and may be missing for the first frames. Pictures keep their shape inside the box; letterbox = false stretches them.

aqua.draw.icon(20, 20, 48, 24, "ak47")
aqua.draw.weapon(20, 60, 120, 90, 7) -- AK-47
aqua.draw.weapon(150, 60, 120, 90, 7, { paint = 44, wear = 0.5 }) -- with a skin
aqua.draw.image(20, 160, 64, 64, "img/logo.png")

weapon options: paint, wear (0 to 1), letterbox, color, clip.

econ keys:

Builder Picture
aqua.econ_key_base(weapon) Weapon without a skin
aqua.econ_key_skin(weapon, paint, [wear]) Weapon with a skin; paint is an id or a name like "asiimov"
aqua.econ_key_mat(path) Sticker, agent or music kit by image path
aqua.draw.econ(20, 20, 120, 90, aqua.econ_key_skin("weapon_ak47", "asiimov"))
aqua.draw.econ(150, 20, 80, 80, aqua.econ_key_mat("econ/stickers/default/stickers1"))

aqua.econ_ready(key) is true once loaded; aqua.econ_miss(key) is true if it never will be.

PNG paths are relative to your script folder and cannot leave it. JPEG is not supported. image options:

Option Meaning
letterbox false stretches the image
src { x, y, w, h }: draw only this part of the image (sprite sheets)
menu_attach x, y are relative to the aqua menu and move with it
clip { x, y, w, h }: draw only inside this rectangle
Function Does
aqua.image_ready(path) Loads the image; true once loaded
aqua.image_size(path) width, height, or nil
aqua.image_miss(path) true if it failed to load
aqua.image_unload(path) Reloads the file on the next draw
aqua.image_delete(path) Unloads and deletes the PNG file

Images over 512 pixels on a side are shrunk; src uses the shrunk size from image_size.