Argument of render_2d (HUD over the game) and render_gui (during screens).
module:event("render_2d", function(render)
render.rect(8, 8, 120, 24, render.paint(0x90101010), 7)
render.text("Hello", 16, 14, 9)
end)Every draw call takes a paint — color, gradient and stroke bundled together —
built with render.paint([color]). See Paints.
The canvas is only valid inside the callback — do not store it.
Drawing
| Function | What it does |
|---|---|
width() / height() |
Screen size in logical pixels |
paint([color]) |
Creates a reusable paint |
rect(x, y, w, h, paint[, radius]) |
Rectangle; radius rounds corners |
shadow(x, y, w, h, paint[, radius[, blur[, noise]]]) |
Blurred rounded rect (drop shadow / glow) |
line(x1, y1, x2, y2, paint[, stroke]) |
Line between two points |
circle(cx, cy, r, paint) |
Filled circle |
text(str, x, y[, size[, paint[, font]]]) |
Text at top-left x, y; returns width |
text_metrics(str[, size[, font]]) |
{width, height, ascent, descent} without drawing |
snapped(v) |
Aligns a logical coordinate to a physical pixel |
physical(v) |
Cancels DPI scale — exact device-pixel size |
begin(mode[, width]) / vertex(x, y[, color]) / finish() |
Arbitrary mesh with per-vertex colors |
save() |
Saves transform/clip state; returns save count |
save_layer([x, y, w, h]) |
Offscreen layer; returns save count |
save_layer_alpha(alpha[, x, y, w, h]) |
Offscreen layer with uniform alpha (0..1) |
restore() |
Undoes the most recent save / save_layer |
restore_to(count) |
Restores to a count from any save call |
translate(dx, dy) |
Shifts subsequent drawing |
scale(sx[, sy[, pivot_x, pivot_y]]) |
Scales (sy defaults to sx) |
rotate(degrees[, pivot_x, pivot_y]) |
Rotates clockwise |
clip(x, y, w, h[, radius[, op]]) |
Clips to a (rounded) rect; op is "intersect" (default) or "difference" |
snapped and physical are also plain globals, so you can use them outside a draw
callback.
radius is one number for all corners, or a {tl, tr, br, bl} table:
render.rect(8, 8, 120, 24, render.paint(0x90101010), { 7, 7, 0, 0 })shadow blurs by blur logical pixels (default 16); noise (0..1) adds film grain.
Draw it behind the rectangle:
render.shadow(8, 8, 120, 24, render.paint(0xA0000000), 7, 24)
render.rect(8, 8, 120, 24, render.paint(0xFF181818), 7)Paints and gradients
render.paint([color]) builds a paint. Setters mutate and return it, so they chain:
local accent
module:event("render_2d", function(render)
accent = accent or render.paint()
:linear_gradient(8, 8, 128, 8, { 0xFF4FF2A6, 0xFF2AA3F2 })
render.rect(8, 8, 120, 24, accent, 7)
render.text("gradient text", 16, 15, 9, accent)
end)| Setter | What it does |
|---|---|
:color(color) |
Base fill/stroke color (when no gradient) |
:linear_gradient(x0, y0, x1, y1, colors[, positions[, tile]]) |
Gradient along (x0,y0) → (x1,y1) |
:radial_gradient(cx, cy, r, colors[, positions[, tile]]) |
Gradient from a center |
:angular_gradient(cx, cy, start_deg, end_deg, colors[, ...]) |
Sweep gradient |
:no_gradient() |
Removes gradient only |
:image(image, x, y, w, h[, tile]) |
Fills with an image mapped to (x, y, w, h) |
:no_image() |
Removes image only |
:stroke([width[, align]]) |
Outline mode; align: "center" (default), "inside", "outside" |
:fill() |
Back to fill mode |
colors is at least two 0xAARRGGBB stops, evenly spaced unless positions (0..1
table of the same length) says otherwise. tile: "clamp" (default), "repeat",
"mirror", "decal".
Gradient coordinates are in the same logical-pixel space as draw calls — they do not move with the shape. Aim the gradient at the shape you fill:
local ring = render.paint()
:angular_gradient(60, 60, 0, 360,
{ 0xFFFF0055, 0xFF00FF55, 0xFF0055FF, 0xFFFF0055 })
:stroke(3)
render.circle(60, 60, 24, ring)
local bar = render.paint():linear_gradient(8, 0, 108, 0,
{ 0x00FFFFFF, 0xFFFFFFFF, 0x00FFFFFF }, { 0, 0.5, 1 })
render.rect(8, 40, 100, 2, bar)A stroke paint turns rect/circle into outlines:
render.rect(x, y, w, h, render.paint(0xFF4FF2A6):stroke(1, "inside"), radius)On text, the gradient fills the glyphs (paint alpha still applies). shadow only
uses the paint’s color.
A paint’s fill is a gradient or an image, never both — setting one clears the other. While an image is set, the paint’s color is ignored but its alpha still fades the image. See Images.
Paints outlive the callback — cache them across frames or build inline each frame.
Meshes
Modes: "points", "lines", "line_strip", "triangles", "triangle_strip".
width is stroke width / point size for line and point modes. Vertex color sticks for
following vertices until changed:
render.begin("triangles")
render.vertex(20, 20, 0xFFFF0055)
render.vertex(80, 20, 0xFF00FF55)
render.vertex(50, 70, 0xFF0055FF)
render.finish()vertex errors if called before begin. finish draws nothing with fewer than two
vertices.
Transforms and layers
Transforms and clips nest with save/restore. Coordinates stay in logical pixels.
save_layer_alpha renders into an offscreen layer and fades it as a whole on restore
— overlapping shapes do not double up the way per-shape alpha would:
render.save_layer_alpha(0.5)
render.rect(8, 8, 60, 24, render.paint(0xFF181818), 7)
render.circle(68, 20, 12, render.paint(0xFF4FF2A6))
render.restore()
render.save()
render.rotate(45, 100, 100)
render.rect(80, 90, 40, 20, render.paint(0xFF4FF2A6), 4)
render.restore()clip defaults to "intersect". Pass "difference" to punch a hole:
render.save()
render.clip(8, 8, 120, 120, 16, "difference")
render.rect(0, 0, 200, 200, render.paint(0xFF181818))
render.restore()Anything left unbalanced at the end of a callback is restored automatically.
Fonts and defaults
| Default | Value |
|---|---|
| Text size | 9 |
| Paint | Opaque white |
| Font | "regular" |
Built-in fonts: "regular", "medium", "consolas", "inter", "small", "icon"
(icon glyphs by name: render.text("bolt", x, y, 9, paint, "icon")), "minecraft"
(vanilla font, resource-pack aware — size is line height, so 18 ≈ GUI scale 2).
Register custom fonts with the fonts global.
render.text anchors at top-left x, y; the baseline is at y + ascent. For layout
(centering, stacking lines), measure first:
local m = render.text_metrics("Hello", 9)
render.text("Hello", x, cy - m.height / 2, 9)