Pixel art and sprites
Draw a small frame pixel by pixel, show it scaled up with hard edges, and animate sprites and tile maps cut from a sheet.
Draw a frame
image.new makes an image, a grid of pixels you draw into. Its origin
is the top-left corner, and Y points down, as in most paint programs.
You do not draw on an image directly. You record commands into a
picture, from image.picture, and Image:apply replays them onto
the image in one pass, so a half-drawn frame never shows. Make the
picture once; each frame, Picture:reset empties it, you record the new
frame, and you apply it again.
This program draws a ball circling a 128 by 96 frame, shown four times larger:
local color = datumhue.color
local W, H = 128, 96
local screen = datumhue.image.new({ width = W, height = H })
screen.filter = "nearest"
screen:mount({ width = W * 4, height = H * 4 })
local pic = datumhue.image.picture()
datumhue.on_update(function()
local t = datumhue.time.now()
local x = 64 + math.cos(t * 2) * 40
local y = 48 + math.sin(t * 3) * 24
pic:reset()
pic:fill(color("#1d2b53"))
pic:circfill(x, y, 8, color("#ff004d"))
pic:print("hello", 46, 80, color("#fff1e8"))
screen:apply(pic)
end)
In this example: datumhue.color datumhue.image.new Image.filter Image:mount datumhue.image.picture datumhue.on_update datumhue.time.now Picture:reset Picture:fill Picture:circfill Picture:print Image:apply
The picture's methods take positions as plain numbers in image pixels:
Picture:circfill is a filled circle, Picture:rectfill a filled
rectangle, Picture:line a line. Picture:camera and Picture:clip
shift and limit everything drawn after them.
Keep pixels sharp
An image or canvas shown larger than its own size is smoothed by
default, which blurs pixel art. Set Image.filter or DrawCanvas.filter
to "nearest", as the program does, to keep every pixel a hard-edged
square. Show the frame at a whole multiple of its size, here 4, so every
pixel is the same size on screen.
Write text
Picture:print writes text in a bitmap font. The built-in font, from
image.font, has 8 by 13 pixel characters. Picture:set_font switches
to a font of your own: an atlas with one cell per character, from the
space on. Picture:measure_text and Picture:wrap size and break text
in the current font.
Animate sprites from a sheet
Image:atlas slices an image into a grid of equal cells, numbered from
0 along each row. Atlas:sprite makes a sprite of one cell, and
DrawCanvas:sprite draws it on a draw canvas. To animate, assign
DrawPrimitive.cell to show another cell of the same sheet.
A sprite drawn this way is a draw shape like any other: it takes a
depth, joins groups, takes a physics body and a shader, and
DrawCanvas:point_query finds it under the pointer.
Lay out a tile map
DrawCanvas:tilemap makes a grid on a canvas whose every cell shows one
cell of an atlas, or nothing. TileMap:set places a tile. Rows count up
from row 0 at the bottom, as Y does on a canvas.
This program paints a sheet of six cells, four frames of a coin and two ground tiles, then lays the ground and spins the coin above it:
local color = datumhue.color
local vec2 = datumhue.math.vec2
local vec3 = datumhue.math.vec3
-- Six 16x16 cells: four frames of a spinning coin, then a
-- grass tile and a dirt tile.
local sheet = datumhue.image.new({ width = 96, height = 16 })
local pic = datumhue.image.picture()
local widths = { 6, 4, 1, 4 }
for i = 0, 3 do
local cx, w = i * 16 + 8, widths[i + 1]
pic:ovalfill(cx - w, 2, cx + w, 13, color("#ffcc00"))
end
pic:rectfill(64, 0, 79, 15, color("#008751"))
pic:rectfill(64, 0, 79, 3, color("#00e436"))
pic:rectfill(80, 0, 95, 15, color("#ab5236"))
sheet:apply(pic)
local atlas = sheet:atlas({
tile_size = vec2(16, 16),
columns = 6,
rows = 1,
})
local canvas = datumhue.draw.new({
width = 128,
height = 128,
background = color("#1d2b53"),
})
canvas.filter = "nearest"
canvas:mount({ width = 384, height = 384 })
-- 8x8 tiles of 16 px cover the canvas. Row 0 is the bottom:
-- dirt, with grass above it.
local map = canvas:tilemap(atlas, { columns = 8, rows = 8 })
for x = 0, 7 do
map:set(x, 0, 5)
map:set(x, 1, 4)
end
-- The coin steps through cells 0 to 3, eight times a second.
local coin = canvas:sprite(atlas:sprite(0), { pos = vec3(0, 0, 1) })
datumhue.on_update(function()
coin.cell = math.floor(datumhue.time.now() * 8) % 4
end)
In this example: datumhue.color datumhue.math.vec2 datumhue.math.vec3 datumhue.image.new datumhue.image.picture Picture:ovalfill Picture:rectfill Image:apply Image:atlas datumhue.draw.new DrawCanvas.filter DrawCanvas:mount DrawCanvas:tilemap TileMap:set DrawCanvas:sprite Atlas:sprite datumhue.on_update DrawPrimitive.cell datumhue.time.now
A sheet usually comes from a file: Bytes:image decodes a PNG, as in
datumhue..
Combine images
Picture:blit copies a rectangle of another image, or of the same one,
into the picture. Its blend mode decides how the copy meets the pixels
already there: an additive blit brightens them for glows and lights,
and a multiplying one darkens them for shadows.
See also
Reference used on this page: datumhue.color datumhue.image.new Image.filter Image:mount datumhue.image.picture datumhue.on_update datumhue.time.now Picture:reset Picture:fill Picture:circfill Picture:print Image:apply datumhue.math.vec2 datumhue.math.vec3 Picture:ovalfill Picture:rectfill Image:atlas datumhue.draw.new DrawCanvas.filter DrawCanvas:mount DrawCanvas:tilemap TileMap:set DrawCanvas:sprite Atlas:sprite DrawPrimitive.cell Picture:line Picture:camera Picture:clip datumhue.image.font Picture:set_font Picture:measure_text Picture:wrap DrawCanvas:point_query Bytes:image Package:dir Picture:blit