The argument of render_2d (over the game) and render_gui (during screens). Colors are
0xAARRGGBB, coordinates are logical pixels, and draw calls are pixel-aligned for you.
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, which bundles color, gradient and stroke. The canvas itself is only valid inside the callback, so do not store it.
Drawing
| Function | Does |
|---|---|
width(), height() |
Screen size in logical pixels |
paint([color]) |
Builds a reusable paint |
rect(x, y, w, h, paint[, radius]) |
Rectangle, radius rounds the corners |
shadow(x, y, w, h, paint[, radius[, blur[, noise]]]) |
Blurred rounded rect for a drop shadow or 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 anchored at top-left, returns its 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 the DPI scale, giving an exact device-pixel size |
begin(mode[, width]), vertex(x, y[, color]), finish() |
Mesh with per-vertex colors |
save() |
Saves transform and clip state, returns the save count |
save_layer([x, y, w, h]) |
Offscreen layer, returns the save count |
save_layer_alpha(alpha[, x, y, w, h]) |
Offscreen layer faded as a whole, alpha is 0..1 |
restore(), restore_to(count) |
Undoes the last save, or unwinds to a save count |
translate(dx, dy) |
Shifts what follows |
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 rect; op is "intersect" (default) or "difference" |
snapped and physical are plain globals too, usable outside a draw callback.
radius is one number for every corner, 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) and noise (0..1) adds film grain.
Draw it before the shape it sits behind:
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 one. 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 | Does |
|---|---|
:color(color) |
Base fill and stroke color when no gradient is set |
:linear_gradient(x0, y0, x1, y1, colors[, positions[, tile]]) |
Gradient along (x0,y0) → (x1,y1) |
:radial_gradient(cx, cy, r, colors[, positions[, tile]]) |
Gradient out from a center |
:angular_gradient(cx, cy, start_deg, end_deg, colors[, ...]) |
Sweep gradient |
:no_gradient() |
Drops the gradient |
:image(image, x, y, w, h[, tile]) |
Fills with an image mapped to that rect |
:no_image() |
Drops the image |
:stroke([width[, align]]) |
Outline mode; align is "center" (default), "inside", "outside" |
:fill() |
Back to fill mode |
colors needs at least two 0xAARRGGBB stops, spaced evenly unless positions (a 0..1
table of the same length) says otherwise. tile is "clamp" (default), "repeat",
"mirror" or "decal".
Gradient coordinates live in the same logical-pixel space as the draw calls and do not follow the shape, so aim the gradient at what it fills:
local bar = render.paint():linear_gradient(8, 0, 108, 0,
{ 0x00FFFFFF, 0xFFFFFFFF, 0x00FFFFFF }, { 0, 0.5, 1 })
render.rect(8, 40, 100, 2, bar)local glow = render.paint():radial_gradient(60, 60, 30, { 0xFF4FF2A6, 0x004FF2A6 })
render.circle(60, 60, 30, glow)local ring = render.paint()
:angular_gradient(60, 60, 0, 360,
{ 0xFFFF0055, 0xFF00FF55, 0xFF0055FF, 0xFFFF0055 })
:stroke(3)
render.circle(60, 60, 24, ring)A stroke paint turns rect and circle into outlines:
render.rect(x, y, w, h, render.paint(0xFF4FF2A6):stroke(1, "inside"), radius)On text a gradient fills the glyphs, with the paint’s alpha still applying. shadow reads
the paint’s color only.
A paint fills with a gradient or an image, never both, and setting one clears the other. While an image is set the color is ignored, though the alpha still fades the image. Paints outlive the callback, so cache them across frames or build them inline.
Meshes
Modes are "points", "lines", "line_strip", "triangles" and "triangle_strip". For
line and point modes, width is the stroke width or point size. A vertex color holds for
every following vertex until you change it:
render.begin("triangles")
render.vertex(20, 20, 0xFFFF0055)
render.vertex(80, 20, 0xFF00FF55)
render.vertex(50, 70, 0xFF0055FF)
render.finish()vertex raises an error before begin, and finish draws nothing with fewer than two
vertices.
Transforms and layers
Transforms and clips nest with save/restore, and coordinates stay in logical pixels.
save_layer_alpha draws into an offscreen layer and fades it as a whole on restore, so
overlapping shapes do not double up the way per-shape alpha does:
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 intersects by default. "difference" punches a hole instead:
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 the callback is restored for you.
Text
Defaults are size 9, opaque white, font "regular".
Built-in fonts: "regular", "medium", "consolas", "inter", "small", "icon" (glyphs
by name, render.text("bolt", x, y, 9, paint, "icon")) and "minecraft" (the vanilla font,
resource-pack aware, where size is line height so 18 matches GUI scale 2). Load your own
with fonts.
render.text anchors at the top-left x, y and puts the baseline at y + ascent. Measure
first when centering or stacking lines:
local m = render.text_metrics("Hello", 9)
render.text("Hello", x, cy - m.height / 2, 9)