Skip to content

Palette swaps

Posted by Mabbees

Nine pixel-art rabbits in a grid. Each rabbit has a different fur color or pattern

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

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.

Two brown pixel-art rabbits. The one on the right is supposed to be peach colored, but it isn't.

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.

A pixel-art rabbit with pink and blue spots

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

Pixel art of a brown rabbit on the left and a peach colored rabbit on the right. All according to plan.

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
PICO-8 Cartridge