In a previous post, I talked about drawing effects and how they can be chained together using function composition. Composing functions is one of my all-time favorite techniques, and there’s another neat application of it in PICO-8: palette swaps.
What is a palette swap?
The concept of a palette swap is drawing something with a different set of colors. It’s a good way to get more mileage out of your PICO-8 sprites. There are a bunch of things you can do with palette swapping
- create variations on a character
- make simple looping animations
- fade in or out of a scene
- simulate day/night cycles
But what does this mean for us as programmers? How do we represent the concept in code?
Palette swaps as tables
One choice of representation is to use a look-up table. Keys in the table tell what color we want to remap, their corresponding values tell what color to map it to. Pretty straightforward. A nice benefit of using tables is that we can pass them directly to the PICO-8 pal
function.
-- a palette defined as a look-up table
my_palette = {
[5] = 2,
[6] = 4,
[7] = 9,
}
pal(my_palette)
Palette swaps as functions
There’s another reasonable choice of representation, and that’s to use a function. At its core, a look-up table is hardly any different from a function. Both are ways of defining a relation, it’s just that a table maps keys to values, whereas a function maps inputs to outputs.
-- a palette defined as a lua function
function my_palette(c)
if c == 5 then return 2 end
if c == 6 then return 4 end
if c == 7 then return 9 end
return c
end
-- apply a remapping to the
-- draw palette
function set_pal(f)
for i=0,15 do
pal(i,f(i))
end
end
set_pal(my_palette)
It’s often more convenient to define palettes as look-up tables than functions. Functions do have other upsides, which we’ll get to in a minute.
Luckily, it’s easy to convert back and forth between these representations. To build a look-up table out of a function, we can iterate through all 16 colors and store the results in a table. To convert a look-up table to a function, we can build a function that checks the table for the input and returns the value it finds.
-- convert a remapping function to a look up table
function pal_to_tbl(f)
local tbl = {}
for i=0,15 do
tbl[i] = f(i)
end
return tbl
end
-- convert a look up table to a remapping function
function tbl_to_pal(tbl)
return function(c)
return tbl[c] or c
end
end
Drawing with palette swaps
Check out this image. The rabbit on the right is drawn with a palette swap, remapping brown to peach.
What’s that, you say? They’re both still brown?! How strange. I definitely applied a palette swap to the second one. Here’s the code to prove it:
alt_palette = {
-- set color 14 (pink) to color 2 (maroon)
[14] = 2,
-- set color 4 (brown) to color 15 (peach)
[4] = 15,
}
function _draw()
cls(6)
-- draw sprite on the left
camera(-40,-64)
draw_bun()
-- draw sprite on the right with a palette swap
camera(-88,-64)
pal(alt_palette)
draw_bun()
-- reset the palette
pal()
end
What I didn’t mention is that the left sprite was already being drawn with a palette swap. The original sprite looks like this. With the right palette swaps, this base sprite can be used to draw all 9 of the rabbits in the grid above.
And here’s the code for draw_bun
.
function draw_bun()
pal({
-- set all fur colors to 4 (brown)
[1] = 4,
[2] = 4,
[5] = 4,
[7] = 4,
})
local x,y = -20,-16
spr(1,x,y,5,4)
end
Admittedly, I cheated to make a point, but the point is it would be nice if we were able to apply a palette swap to arbitrary drawings, not just to original drawings. I want the concept of palette swapping to be general and composable.
Combining palettes
How can we change the code to get the expected result for the second sprite — namely, a peach colored rabbit?
We have a palette we can use to draw a brown rabbit and a palette to change brown to peach. What we still need is a way to combine both palette effects. This is where the function representation for palette swaps really shines. Combining palette swaps is function composition!
-- compose two functions
-- can be used to chain two palette swaps together
function compose(f,g)
return function(x)
return g(f(x))
end
end
brown_bun = tbl_to_pal({
-- set all fur colors to 4 (brown)
[1] = 4,
[2] = 4,
[5] = 4,
[7] = 4,
})
peach_bun = tbl_to_pal({
-- set color 14 (pink) to color 2 (maroon)
[14] = 2,
-- set color 4 (brown) to color 15 (peach)
[4] = 15,
})
function draw_bun()
local x,y = -20,-16
spr(1,x,y,5,4)
end
function _draw()
cls()
-- draw brown bunny on the left
camera(-24,0)
pal(pal_to_tbl(brown_bun))
draw_bun()
-- draw peach bunny on the right
camera(24,0)
pal(pal_to_tbl(
compose(brown_bun, peach_bun)
))
draw_bun()
-- reset the palette
pal()
end
And with that, we’ve fixed the bug. Combining the two palette swaps gives us the peach colored rabbit we wanted.
Refactoring
The code above produces the results I want, but not without making some significant changes to the code’s structure. Ideally, I want a way to define the draw_bun
function in a self-contained way that includes the palette swapping. Then I’d like to draw the peach colored rabbit as a variation on that function. That is to say, I want drawing with a palette swap to be a composable effect.
Here’s a draw_with_palette
function we can use to manage palette swaps, even repeated ones. It makes use of a state variable, draw_pal
, to track what palette-swap we’re currently drawing with. It also takes care of combining palette swaps, and restoring the old palette afterwards. As long as we avoid calling pal
directly anywhere else, this function handles all our composable palette-swapping needs!
function id(x) return x end
draw_pal = id
function draw_with_palette(p)
return function(draw)
return function()
local prev = draw_pal
-- combine the old palette with the new one
draw_pal = compose(p,draw_pal)
pal(pal_to_tbl(draw_pal))
draw()
-- restore the old palette
draw_pal = prev
pal(pal_to_tbl(draw_pal))
end
end
end
-- usage:
brown_bun = tbl_to_pal({
-- set color 5 (gray) to color 4 (brown)
[5] = 4,
-- set color 7 (white) to color 4 (brown)
[7] = 4,
})
peach_bun = tbl_to_pal({
-- set color 14 (pink) to color 2 (maroon)
[14] = 2,
-- set color 4 (brown) to color 15 (peach)
[4] = 15,
})
draw_bun = draw_with_palette(brown_bun)(function()
local x,y = 24,24
spr(1,x,y,2,2)
end)
function _draw()
cls()
-- draw brown bunny on the left
camera(-24,0)
draw_bun()
-- draw peach bunny on the right
camera(24,0)
draw_with_palette(peach_bun)
(draw_bun)()
end
Putting it all together
Having a composable palette swapping system is great for getting the most out of your sprites. It dovetails nicely with the system of drawing effects from my post Suspend Your Draw Calls!
Here’s the code for the rabbit grid from earlier. Each one is drawn with a combination of two palette swaps. One to select which fur pattern to use, and the other to choose the colors.
function draw_bun()
local x,y = -20,-16
spr(1,x,y,5,4)
end
--patterns
pal_solid = tbl_to_pal({
[1]=5,
[2]=5,
[5]=5,
[7]=5,
})
pal_dutch = tbl_to_pal({
[1]=5,
[2]=7,
})
pal_spotted = tbl_to_pal({
[1]=5,
[2]=5,
[5]=7,
})
--colors
pal_gray = id
pal_orange = tbl_to_pal({
[5] = 9,
})
pal_brown = tbl_to_pal({
[5] = 4,
[7] = 15
})
--buns
solid_bun = draw_with_palette(pal_solid)
(draw_bun)
dutch_bun = draw_with_palette(pal_dutch)
(draw_bun)
spotted_bun = draw_with_palette(pal_spotted)
(draw_bun)
function _draw()
cls(6)
camera(-24,-24)
draw_with_palette(pal_gray)
(solid_bun)()
camera(-64,-24)
draw_with_palette(pal_gray)
(dutch_bun)()
camera(-104,-24)
draw_with_palette(pal_gray)
(spotted_bun)()
camera(-24,-64)
draw_with_palette(pal_orange)
(solid_bun)()
camera(-64,-64)
draw_with_palette(pal_orange)
(dutch_bun)()
camera(-104,-64)
draw_with_palette(pal_orange)
(spotted_bun)()
camera(-24,-104)
draw_with_palette(pal_brown)
(solid_bun)()
camera(-64,-104)
draw_with_palette(pal_brown)
(dutch_bun)()
camera(-104,-104)
draw_with_palette(pal_brown)
(spotted_bun)()
end
Reference
If you’d like to use these functions in your own carts, here’s a summary of the library code from this article. You can also check out the demo cart for example usage.
I’m licensing both under CC BY 4.0.
function id(x) return x end
function compose(f,g)
return function(x)
return g(f(x))
end
end
function pal_to_tbl(f)
local ret={}
for i=0,15 do
ret[i] = f(i)
end
return ret
end
function tbl_to_pal(tbl)
return function(c)
return tbl[c] or c
end
end
draw_pal = id
function draw_with_palette(p)
return function(draw)
return function()
local prev = draw_pal
draw_pal = compose(p,draw_pal)
pal(pal_to_tbl(draw_pal))
draw()
draw_pal = prev
pal(pal_to_tbl(draw_pal))
end
end
end