A custom font

This post was published in 2018 and is kept here for reference. It may contain information that's outdated or inaccurate.

Out of the box Pico-8 has one font. It’s the utilitarian answer to most problems. 3px by 5px and fairly legible.

This font isn’t always good enough. How about making your own font?

I’ve seen lots of approaches for creating fonts in Pico-8 – mostly using sprites for each glyph of the font, but then you’re limited to 8x8px and a have to mess with pal modes if you want to adjust colors on-the-fly.

My approach uses the raw pixel data encoded in strings that are rendered using calls to pset, falling back to the built-in font for any missing glyphs.

First, here are my helper functions…

-- merge tables
function mt(t1,t2)
 local new = {}
 for k,v in pairs(t1) do new[k] = v end
 for k,v in pairs(t2) do new[k] = v end
 return new
end
-- less verbose way to do ternary operations
function ternary(cond ,t ,f )
 if cond then return t else return f end
end

I had to come up with a compact way to represent the glyphs. Initially I used something like:

{ glyph = "o", pixels = {
    0,1,1,1,0,
    1,0,0,0,1,
    1,0,0,0,1,
    1,0,0,0,1,
    1,0,0,0,1,
    1,0,0,0,1,
    0,1,1,1,0,
}}

But that contains a lot of tokens and lots of repeated/dead values to represent the empty pixels in a glyph.

Eventually I moved to something like:

{"o",  "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"}

That’s the same pixel data, but encoded in to a much shorter string.

The first number indicates our starting parity – this can be 0 or 1 and tells us if the top/left pixel is empty or filled.

Each time we have a . the parity flips, so with the example of our “o” glyph it starts with 0 for 1 count, then 1 for 3 counts, 0 for 1 count, 1 for 1 count, etc.

When the remaining pixels in a glyph are 0 we can just omit them from the glyph data as they’re not drawn.

The real wins are in the more simple/empty glyphs, such as “.” for example.

Before:

{ glyph = ".", pixels = {
    0,0,0,0,0,
    0,0,0,0,0,
    0,0,0,0,0,
    0,0,0,0,0,
    0,0,0,0,0,
    0,0,0,0,0,
    0,0,1,0,0,
}}

After:

{".", "0.32.1"}

Making this change took me from an initial token count of roughly 2800 to a much more manageable count of 560-ish.

So with that technical talk out of the way, here’s the complete glyph set in code, and the function to render text. I’ll give some usage examples after.

global_y = 0

function write(str,opts)
 opts = opts or {}
 _opts = {
  x=0,
  y=global_y,
  c=7
 }
 opts = mt(_opts,opts)
 font = {
  w=5,
  h=7,
  -- first num indicates starting value, 0 or 1
  -- each number indicates number of 0 or 1 in a row
  -- flips at each .
  glyphs = {
   {"0",  "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"},
   {"1",  "0.2.1.3.2.2.1.1.1.4.1.4.1.4.1.2.5"},
   {"2",  "0.1.3.1.1.3.1.4.1.3.1.3.1.3.1.3.5"},
   {"3",  "0.1.3.1.1.3.1.4.1.2.2.5.2.3.1.1.3"},
   {"4",  "0.3.2.2.1.1.1.1.1.2.2.3.6.4.1.4.1"},
   {"5",  "1.6.4.1.5.3.5.2.3.1.1.3"},
   {"6",  "0.1.3.1.1.3.2.4.4.1.1.3.2.3.1.1.3"},
   {"7",  "1.5.4.1.4.1.3.1.3.1.3.1.3.1"},
   {"8",  "0.1.3.1.1.3.2.3.1.1.3.1.1.3.2.3.1.1.3"},
   {"9",  "0.1.3.1.1.3.2.3.1.1.4.4.2.3.1.1.3"},
   {"a",  "0.1.3.1.1.3.2.3.7.3.2.3.2.3.1"},
   {"b",  "1.4.1.1.3.2.3.5.1.1.3.2.3.5"},
   {"c",  "0.1.3.1.1.3.2.4.1.4.1.4.1.3.1.1.3.1"},
   {"d",  "1.4.1.1.3.2.3.2.3.2.3.2.3.5"},
   {"e",  "1.6.4.1.4.4.1.1.4.1.4.5"},
   {"f",  "1.6.4.1.4.4.1.1.4.1.4.1"},
   {"g",  "0.1.3.1.1.3.2.3.2.4.1.1.4.3.1.1.3"},
   {"h",  "1.1.3.2.3.2.3.7.3.2.3.2.3.1"},
   {"i",  "1.5.2.1.4.1.4.1.4.1.4.1.2.5"},
   {"j",  "1.5.2.1.4.1.4.1.2.1.1.1.2.1.1.1.3.2"},
   {"k",  "1.1.3.2.2.1.1.1.1.1.2.2.3.1.1.1.2.1.2.1.1.1.3.1"},
   {"l",  "1.1.4.1.4.1.4.1.4.1.4.1.4.5"},
   {"m",  "1.1.3.3.1.3.1.1.1.2.3.2.3.2.3.2.3.1"},
   {"n",  "1.1.3.3.2.2.1.1.1.2.2.3.3.2.3.2.3.1"},
   {"o",  "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"},
   {"p",  "1.4.1.1.3.2.3.5.1.1.4.1.4.1"},
   {"q",  "0.1.3.1.1.3.2.3.2.3.2.3.1.1.3.5.1"},
   {"r",  "1.4.1.1.3.2.3.5.1.1.1.1.2.1.2.1.1.1.3.1"},
   {"s",  "0.1.3.1.1.3.2.5.3.5.2.3.1.1.3"},
   {"t",  "1.5.2.1.4.1.4.1.4.1.4.1.4.1"},
   {"u",  "1.1.3.2.3.2.3.2.3.2.3.2.3.1.1.3"},
   {"v",  "1.1.3.2.3.1.1.1.1.1.2.1.1.1.2.1.1.1.3.1.4.1"},
   {"w",  "1.1.3.2.3.2.3.2.1.1.1.2.1.1.1.2.1.1.1.1.1.1.1.1"},
   {"x",  "1.1.3.2.3.1.1.1.1.1.3.1.3.1.1.1.1.1.3.2.3.1"},
   {"y",  "1.1.3.1.1.1.1.1.3.1.4.1.4.1.4.1.4.1"},
   {"z",  "1.5.4.1.3.1.3.1.3.1.3.1.4.5"},
   {" ",  "0.35"},
   {".",  "0.32.1"},
   {",",  "0.27.1.3.1"},
   {"!",  "0.2.1.4.1.4.1.4.1.4.1.9.1"},
   {"?",  "0.1.3.1.1.3.1.4.1.3.1.3.1.9.1"},
   {"'",  "0.3.1.3.1"},
   {"\"", "0.1.1.1.1.1.1.1.1"},
   {":",  "0.12.1.14.1"},
   {";",  "0.12.1.14.1.3.1"},
   {"(",  "0.3.1.3.1.3.1.4.1.4.1.5.1.5.1"},
   {")",  "0.1.1.5.1.5.1.4.1.4.1.3.1.3.1"},
   {"[",  "0.1.3.2.1.4.1.4.1.4.1.4.1.4.3"},
   {"]",  "0.1.3.4.1.4.1.4.1.4.1.4.1.2.3"},
   {"{",  "0.2.2.3.1.4.1.3.2.4.1.4.1.4.2"},
   {"}",  "0.1.2.4.1.4.1.4.2.3.1.4.1.3.2"},
   {"<",  "0.3.1.3.1.3.1.3.1.5.1.5.1.5.1"},
   {">",  "0.1.1.5.1.5.1.5.1.3.1.3.1.3.1"},
   {"-",  "0.16.3"},
   {"+",  "0.12.1.3.3.3.1"},
   {"/",  "0.4.1.3.1.4.1.3.1.3.1.4.1.3.1"},
   {"\\", "1.1.5.1.4.1.5.1.5.1.4.1.5.1"},
  }
 }

 _x = opts.x
 _y = opts.y
 letters = {}
 for i=1,#str do
  letter = sub(str,i,i)
  add(letters,letter)
 end
 for k,v in pairs(letters) do
  letter = nil
  for _glyph in all(font.glyphs) do
   if _glyph[1] == v then
    letter = _glyph[2]
   end
  end
  local _sx = _x
  local _sy = _y
  if letter != nil then
   local _px = _sx
   local _py = _sy
   pixels = {}
   local _p = sub(letter,1,1)
   local prev = nil
   for i=2,#letter do
    cur = sub(letter,i,i)
    if type(prev) == "number" and cur != "." then
     cur = sub(letter, i-1, i)
    end
    if cur == "." then
     _p = ternary(_p == "0", "1", "0")
    else
     for j=1,tonum(cur) do
      add(pixels, _p)
     end
    end
    prev = tonum(cur)
   end
   for i,p in pairs(pixels) do
    i = i - 1
    if i % font.w == 0 then
     _py += 1
     _sx = _x - (font.w * (i / font.w))
    end
    _px = _sx + i
    if p == "0" then
     pset(_px, _py, opts.c)
    end
   end
  else
   font.w = 8
   print(v, _sx, _y + ((font.h - 2)/2), opts.c)
  end
  _x += font.w + 1
 end
 global_y += font.h + 2
end

If you’re still with me, congratulations! Here is how you use the function:

function _draw()
 cls()
 global_y = 0 -- reset on each draw call
 write("hello world")

 -- passing a color is possible!
 write("this is max 🐱", {c=12})

 write("my font brings all")
 write("the devs to the yard")
 write("0123456789")
 write("abcdefghijklm")
 write("nopqrstuvwxyz")
 write(".,?!:;()[]{}-+/\\<>")
 write("yes, this is a lot")
 write("nicer to read! ♥")
 write("")
 write("█▒🐱⬇️░✽●♥☉웃⌂⬅️😐")
 write("♪🅾️◆…➡️★⧗⬆️ˇ∧❎▤▥")
 write("@#$%^&*")
end

And this is how it looks:

This post is also available in plain text

[Webmentions]

Want to reply? I've hooked up Webmentions, so give it a go!