Canvas
Canvas is Basalt's low-level drawing element. Use it when application data is best rendered as rows of terminal cells instead of hundreds of child elements: heatmaps, editors, maps, compact charts, and custom controls.
Create a Canvas
local canvas = frame:addCanvas({
x = 2,
y = 3,
width = 30,
height = 10,
background = colors.black,
})
canvas.draw = function(self, buffer)
buffer:blit(2, 2, "Custom surface", colors.white, colors.blue)
endThe callback receives the Canvas and Basalt's Render buffer. Coordinates are local and start at 1, 1.
The buffer is not a CC terminal. Read dimensions from the Canvas:
canvas.draw = function(self, buffer)
local width, height = self.width, self.height
endDo not call buffer:getSize().
Drawing Methods
| Method | Purpose |
|---|---|
fill(x, y, w, h, ch, fg, bg) | Fill a rectangle |
blit(x, y, text, fg, bg) | Draw one text span |
drawText(x, y, text) | Replace text while preserving colors |
colorBlit(x, y, text, foregrounds, backgrounds) | Draw public colors per cell |
drawBlit(x, y, text, fgHex, bgHex) | Draw ordinary CC blit strings |
For a dense surface, emit one colorBlit() call per visible row:
buffer:colorBlit(
1,
1,
" ",
{ colors.white, colors.white, colors.white },
{ colors.green, colors.orange, colors.red }
)drawBlit() is useful with terminal lines:
local text, foreground, background = window.getLine(1)
buffer:drawBlit(1, 1, text, foreground, background)Redraw After Model Changes
Canvas data is usually an ordinary Lua table. Basalt cannot observe it, so request a render after changing paint-only data:
cells[y][x] = colors.orange
canvas:markRenderDirty()Use markDirty() only when the change can affect layout. For a Signal-driven Canvas, subscribe explicitly:
local level = basalt.state(0)
local unsubscribe = level:subscribe(function()
canvas:markRenderDirty()
end)Keep the returned function when the subscription is temporary.
Input
Canvas inherits normal element events. Mouse coordinates are already local:
canvas:onClick(function(self, button, x, y)
cells[y][x] = button == 2 and colors.black or colors.orange
self:markRenderDirty()
end)Use the same coordinate-to-model calculation for drawing and input.
Runnable Heatmap
local basalt = require("basalt")
local frame = basalt.getMainFrame()
frame.background = colors.black
local mapWidth, mapHeight = 32, 12
local cells = {}
for y = 1, mapHeight do
cells[y] = {}
for x = 1, mapWidth do
cells[y][x] = 0
end
end
frame:addLabel({
x = 2,
y = 1,
width = 47,
text = "Click or drag to paint; right-click erases",
foreground = colors.orange,
})
local canvas = frame:addCanvas({
x = 2,
y = 3,
width = mapWidth,
height = mapHeight,
background = colors.black,
})
local heatColors = {
colors.black,
colors.blue,
colors.purple,
colors.red,
colors.orange,
colors.yellow,
}
canvas.draw = function(self, buffer)
for y = 1, self.height do
local foregrounds, backgrounds = {}, {}
for x = 1, self.width do
foregrounds[x] = colors.white
backgrounds[x] = heatColors[cells[y][x] + 1]
end
buffer:colorBlit(
1,
y,
string.rep(" ", self.width),
foregrounds,
backgrounds
)
end
end
local function paint(self, button, x, y)
if x < 1 or x > mapWidth or y < 1 or y > mapHeight then return end
cells[y][x] = button == 2 and 0 or math.min(5, cells[y][x] + 1)
self:markRenderDirty()
end
canvas:onClick(paint)
canvas:onDrag(paint)
frame:addButton({
x = 37,
y = 4,
width = 12,
height = 2,
text = "Clear",
}):onClick(function()
for y = 1, mapHeight do
for x = 1, mapWidth do cells[y][x] = 0 end
end
canvas:markRenderDirty()
end)
frame:addButton({
x = 37,
y = 8,
width = 12,
height = 2,
text = "Exit",
background = colors.red,
}):onClick(function()
basalt.stop()
end)
basalt.run()One Canvas replaces 384 one-cell elements. The model remains a plain Lua table and the draw callback writes twelve rows.
Common Mistakes
buffer:getSize()does not exist; useself.widthandself.height.- Mutating Canvas data does not redraw automatically.
rawBlit()uses Basalt's internal palette bytes; ordinary CC blit strings belong indrawBlit().- Avoid doing file IO or peripheral calls inside
draw.
Related Guides and API
Next Steps
Use Hosted Programs when the content already behaves like a complete CC application with its own terminal and event loop.