Skip to content

2D canvas

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)
Navigation

Type to search…

↑↓ navigate↵ selectEsc close