
# 2D canvas

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.

```lua
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](#paints-and-gradients), 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[, u, v]])`, `finish([paint])` | [Mesh](#meshes) with per-vertex colors, optionally textured |
| `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:

```lua
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:

```lua
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:

```lua
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](image.md) 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:

### tab: Linear

```lua
local bar = render.paint():linear_gradient(8, 0, 108, 0,
    { 0x00FFFFFF, 0xFFFFFFFF, 0x00FFFFFF }, { 0, 0.5, 1 })
render.rect(8, 40, 100, 2, bar)
```

### tab: Radial

```lua
local glow = render.paint():radial_gradient(60, 60, 30, { 0xFF4FF2A6, 0x004FF2A6 })
render.circle(60, 60, 30, glow)
```

### tab: Angular

```lua
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:

```lua
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:

```lua
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.

Pass a paint with an [image](image.md) to `finish` and the mesh is textured by it. Every vertex
then needs `u, v`, which run `0..1` across the paint's image rect, and each texel is multiplied
by the vertex color:

```lua
local atlas = surface.new(64, 64)
local paint

module:event("render_2d", function(render)
    local image = atlas:image()
    if not image then return end
    paint = paint or render.paint():image(image, 0, 0, 64, 64)
    render.begin("triangles")
    render.vertex(20, 20, 0xFFFFFFFF, 0, 0)
    render.vertex(84, 20, 0xFFFFFFFF, 1, 0)
    render.vertex(20, 84, 0x80FFFFFF, 0, 1)
    render.finish(paint)
end)
```

## 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:

```lua
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:

```lua
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`](fonts.md).

`render.text` anchors at the top-left `x, y` and puts the baseline at `y + ascent`. Measure
first when centering or stacking lines:

```lua
local m = render.text_metrics("Hello", 9)
render.text("Hello", x, cy - m.height / 2, 9)
```
