A sprite editor in 100 lines of code


Recently I've been mulling ways to add images to my Lua Carousel scripts. One problem with LÖVE apps on mobile devices is a paucity of ways to get non-text data in and out. We don't have https support until LÖVE v12. On Android there are also onerous restrictions on sharing data through files. Finally, we don't have ways to access non-text data from the clipboard. For now, small amounts of text are the sweet spot for LÖVE and therefore for Lua Carousel. For images, that translates to small sprites that can be economically encoded as text.

For example, here's a tiny program to draw a sprite on screen:

sprite = {
  {1,2},
  {2,1},
}
function draw_sprite(sprite, zoom, x,y, palette)
  for j,row in ipairs(sprite) do
    for i,cell in ipairs(row) do
      color(unpack(palette[cell]))
      rect('fill', x+(i-1)*zoom, y+(j-1)*zoom, zoom, zoom)
    end end end
draw_sprite(sprite, 16, 40,90,
  {{1,0,0}, {0,0,1}})

It's a 2D array of ints, and the ints index into a palette where you configure colors.

For many purposes I can edit the sprite right in the source code. But it's also been fun for my kids to play with the following sprite editor:

palette = {{1,0,0}, {0,0,1}}
W,H = 8,8
sprite = {}
for i=1,H do
  local row = {}
  for j=1,W do
    table.insert(row, (i+j)%#palette + 1)
  end
  table.insert(sprite, row)
end
zoom = 32  -- pixels
-- top-left of sprite
X0,Y0 = 40, 90
widgets = {}
-- one button per pixel of sprite
function sprite_pixel(X,Y)
  local x = X0 + (X-1)*zoom
  local y = Y0 + (Y-1)*zoom
  local draw = function()
    color(unpack(palette[sprite[Y][X]]))
    rect('fill', x,y, zoom-1,zoom-1)
  end
  local ispress = function(x2,y2)
    return x2 >= x and x2 <= x+zoom and y2 >= y and y2 <= y+zoom
  end
  local press = function()
    sprite[Y][X] = sprite[Y][X]%#palette + 1
  end
  return {draw=draw, ispress=ispress, press=press}
end
for y=1,H do
  for x=1,W do
    table.insert(widgets, sprite_pixel(x,y))
  end
end
-- one slider per color component in palette
function palette_component(p, c)
  local x0, x1 = X0+20, 200  -- left and right limit
  local lo, hi = 0, 1  -- what left and right map to
  local x = x0 + (x1-x0)*(palette[p][c]-lo)/(hi-lo)
  local top = Y0+zoom*H + 30
  local y = top + (p-1)*100 + (c-1)*30
  local w,h = 20,20
  local selected = false
  local slider_color = {0,0,0}
  slider_color[c] = 1
  local draw = function()
    color(unpack(slider_color))
    line(x0,y, x1,y)
    rect('fill', x-w/2, y-h/2, w,h)
  end
  local ispress = function(x2,y2)
    return x2 >= x-w/2 and x2 <= x+w/2 and y2 >= y-h/2 and y2 <= y+h/2
  end
  local press = function() selected = true end
  local update = function(x2,y2)
    if selected then
      x = min(max(x2, x0), x1)
      palette[p][c] = lo + (x-x0)*(hi-lo)/(x1-x0)
    end
  end
  local release = function() selected = false end
  return {draw=draw, ispress=ispress, press=press, update=update, release=release}
end
for p=1,#palette do
  for c=1,#palette[1] do
    table.insert(widgets, palette_component(p, c))
  end
end
function car.draw()
  for name,w in pairs(widgets) do
    w.draw()
  end
end
function car.mousepressed(x,y, b)
  for name,w in pairs(widgets) do
    if w.ispress(x,y) then
      return w.press()
    end
  end
end
function car.update(dt)
  for name,w in pairs(widgets) do
    if w.update then w.update(App.mouse_x(), App.mouse_y()) end
  end
end
function car.mousereleased(x,y, b)
  for name,w in pairs(widgets) do
    if w.release then w.release() end
  end
end

If you try pasting either of these programs into Lua Carousel, remember to first run the abbreviations on one of the example screens. Or if you've deleted that screen, here are the abbreviations I used in this post:

g = love.graphics
line, rect = g.line, g.rectangle
color = g.setColor
min, max = math.min, math.max

Get Lua Carousel

Leave a comment

Log in with itch.io to leave a comment.