---
title: "Surfaces"
description: "CPU pixel buffers with per-pixel primitives and indexed textures, uploaded to a paint as an image."
---

> Documentation Index
> Fetch the complete documentation index at: https://aesthetic-docs.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Surfaces

A surface is a CPU pixel buffer you draw into pixel by pixel, then hand to a paint as an
[image](/image). The [canvas](/canvas) draws shapes; a surface draws pixels, which is what
you want for procedural effects, plots, custom overlays and pixel art. The inner loops run
outside Lua: your script decides what to draw, the surface fills the pixels.

```lua
local screen = surface.new(160, 90)

module:event("render_2d", function(render)
screen:clear(0xFF101418)
local t = os.clock()
for x = 0, 159 do
    local h = 45 + math.floor(math.sin(x * 0.08 + t * 2) * 25)
    screen:line(x, 89, x, h, 0xFF3A7BD5)
end
screen:circle(80, 30, 12, 0xFFFFC857, true)
local image = screen:image()
if image then
    render.rect(20, 20, 480, 270, render.paint():image(image, 20, 20, 480, 270))
end
end)
```

:::caution
A surface is freed when the script unloads. Build it once at top level or on `enable`, never
per frame.
:::

## Drawing

Colors are plain `0xAARRGGBB` and every primitive alpha-blends over what is already there.

| Function | Does |
|----------|------|
| `width()`, `height()` | Buffer size in pixels |
| `clear([argb])` | Fills every pixel, fully transparent without an argument |
| `pixel(x, y[, argb])` | Writes one pixel, or reads it without a color (`nil` off-buffer) |
| `rect(x, y, w, h, argb)` | Filled rect |
| `line(x1, y1, x2, y2, argb)` | Bresenham line |
| `circle(x, y, r, argb[, fill])` | Outline, or a disc with `fill` |
| `draw(src, x, y[, w, h])` | Another surface, nearest-neighbour scaled |
| `tint(argb)` | Blends a color over everything |
| `image()` | Uploads and returns an [image](/image) handle |

A new surface starts fully transparent and the alpha channel survives the upload, so clearing
to a translucent color (or not clearing at all) composites over the game like any other paint,
with no opaque backdrop needed.

`draw` reads from another surface, so buffers compose: rasterize a sprite once into its own
small surface, then stamp it into the screen buffer every frame at whatever size you need.

## Indexed textures

A surface can also rasterize indexed textures, where texels hold palette indices (`0..255`,
`-1` transparent) and a palette turns them into pixels. Swapping palettes recolors a whole
frame without touching a pixel.

| Call | Does |
|------|------|
| `surface:palette(bytes[, index])` | 256-color palette from `768 * n` RGB bytes; `index` picks one inside the blob |
| `surface:colormaps(bytes)` | Light ramps: `256 * n` bytes, where entry `light * 256 + texel` remaps an index |
| `surface:lights()` | How many light levels the current ramp has, `1` without one |

Every textured primitive takes a `light` argument, the ramp row to look the texel up in. `0`
is normally the brightest.

| Function | Does |
|----------|------|
| `column(x, y0, y1, tex, col, frac, step[, light])` | Vertical texture run |
| `span(y, x0, x1, tex, u, v, du, dv[, light])` | Horizontal texture run |
| `blit(x, y, w, h, tex[, light[, flip]])` | Whole texture, nearest-neighbour scaled |

`column` walks the texture down one screen column, where `frac` is the texture row at `y0` and
`step` the row delta per screen pixel. `span` walks it across one row, where `u, v` are the
coordinates at `x0`, stepped by `du, dv`. Both wrap around the texture, skip transparent
texels and clip to the buffer, so off-screen ends cost nothing.

## Loaders

| Loader | Builds |
|--------|--------|
| `surface.rgba(w, h, bytes)` | A surface from `w * h * 4` raw RGBA bytes |
| `surface.patch(bytes)` | Masked column-format indexed picture, keeping its sprite offsets |
| `surface.flat(bytes[, size])` | Raw row-major square of indices, 64×64 by default, which is what `span` samples |
| `surface.composite(w, h, parts)` | Patches pasted into one texture |

`parts` is a list of `{ patch = <texture>, x = <integer>, y = <integer> }`, and texels no
patch covers stay transparent. `column` and `blit` take any texture; `span` needs a row-major
one from `surface.flat`.

```lua
local patch = surface.patch(bytes)
local w, h = patch:width(), patch:height()
local ox, oy = patch:offset()
```

## Uploading

`surface:image()` copies the buffer to the GPU and returns an [image](/image) handle sampled
nearest, so pixels stay crisp. Call it from a render callback, since it needs the render
thread, and only when the pixels changed: the handle keeps showing the last upload, so a
buffer you redraw at 30 Hz inside a 240 Hz render loop uploads 30 times, not 240.

Source: https://aesthetic-docs.pages.dev/surface/index.md
