Log In  


PicoFont

PicoFont aims to solve a single problem I have experienced with Picotron. That being a singular, small font scale. As far as I know, no way to increase the font size exists. This is okay for most things, but for some use cases this limits functionality. That is why I am introducing PicoFont into the PicoUI family (kind of). PicoFont is a simple and declarative font renderer.

Font definitions are comprised of a single table:

include "font.lua"

local simple = {
		meta = {
			name = "Simple Font",
			author = "BlueFalconHD"
		},

		-- To get the pod, hold down shift and select all of the text sprites for your 
		-- font. Then paste it here as a pod, add a comma at the end, and define your 
		-- character properties.

		pod = --[[pod_type="gfx",region_w=8]]unpod("b64:bHo0AA0EAADSFgAA8xx7e2JtcD1weHUAQyAFBQQAJwAHIBcgFxAXABcAByxmbGFncz0wLHBhbl94CADIeT0wLHpvb209OH0sPQBACAQHMAIAETdCABEnQgAPQQAaBH4AfzAHIAcAJwB9ABwBfAA-BwBHvgAkA30AP2dANzcAGcAECAQQBxAHAAcABxD4AAECAB8QugAc7wAXABcQJyAHADcwBzAX-AAfEgd5AQFHABAX-wAfIPsAGl8BBgQHAPYAHTIgB2DuAD8XEAfoARwxBAcEOwBjEBcABwAXPQEPsgAaMQIHBDYAHwA8ACBDBQUEFzoAAYAADy4BHT8FBQRqASYF4gIvIBfiAiEEeABCRwAHMAIAD2kCKQECAA-6ACMPfAAfjwUEAEdAJ0BH8wAaMAMHBN4BADMBAGUDD6gBHy8gFw0EJgSlAS8HANEDHiUFBF0CAwQAAgIAD4IAHgN_AAQIAA8uAh0EwAARED4AEjAEAA_FABwxRyAHAgAfR30AHAPnAhFnRgEfF7wAHgJDBhFHBgAACAAPfgAjAagCD2sDIwKBAAQCAA_BAB4SVwcHAaYDH0c8ACAiJxA8AC8HMPoAJi8QJ-oAIwi3AQ_5ARwzAwgEqwMCAgAvACd8ABwkRyD3AAG3Aj8QFxC9ACAAUgYiJxD2Ag-DAB4FxQgvBzC_ASCABwgEB0AnICd2AwDaBjIQF0ACAA_EAB4BPwkRB64FHhfGAA8JAhgDAgAPAwMmL0cAigIsAgIAAAwBLwAHggAlDwQBKj9AJ0ACASQVR4YDAQIAD40CIg_CASkGQQAHyAUfEJECHgKDAgGDAQMEAACWBh8XjgAhAYgAIyAHCAAOlAEPRgAaHzBYASQQR8sGAgIAD6EDHTMDCASaAw_jBCUiACe7AABIAQ6AAA9BABg-ACdA7QsiIAgEtwAEBAAFMgUPogYiEEA7AA9eByQD5w0PWQMjFDCPCA8BAiICgQABeQAPgwAmBUQADwsLIwNBAAimBQ_FAB0mAwPFDA9MCx0ANQAXRwYAD_8NGk8IBFcA7gAiIwcwdQM-gAcQ6gAeD4ULHj8BAwSdAxpPAgMEAIsOHT8BAQSUABoQAnwBHyBmAB1PBAEEN9EPHCEQB-gGAKwNH0f0BRwBnQ83IAcQBAAfIEAAHScXEEAAPxAXIC4CHSMXEKEEEBCwDgCiAg8CCCARArEOFAcCAB8QPQAdAYMAAwIADxcCGzAFBQT7Bz8QRxCeBB1PBgEEV2oAGk8DBEdAwwYeIRcA3AABnQE-BxAXPwAcA90BAEQAD7wRHhEC4wsFAgAfFzwAHAfPAS8AJ7UEHB93xQIcA7AEBAQAD0sETTIDAgTZAA8REiJPADdANx0FHA9rABxfEBAE8PAxAM1QbT04fX0="),

		-- This just maps a specific sprite in the pod above to character.
		-- Determines the sprite to show for a string. Any spaces are ignored sprites

		chars = {
			"abcdefgh",
			"ijklmnop",
			"qrstuvwx",
			"yzABCDEF",
			"GHIJKLMN",
			"OPQRSTUV",
			"WXYZ1234",
			"567890*#",
			"!?\" '.;-",
			"$/%&()+_",
			"={}[]|\\,",
			"^@:     ",
		},

		-- Y-offset of the sprite when displayed. Useful for characters that
		-- don't match the height of the rest or that have descenders.

		ascent = {
			-3,0,-3,0,-3,0,-3,-1,
			-2,-2,-1,-1,-3,-3,-3,-3,
			-3,-3,-3,-1,-3,-3,-3,-3,
			-3,-3,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,0,0,0,0,-3,-2,
			0,0,0,0,0,-7,-2,-4,
			-1,0,0,0,0,0,-2,-7,
			-3,0,0,0,0,0,0,-7,
			0,-3,-4,0,0,0,0,0,
		},

		-- x-offset of sprites
		-- somewhat finnicky as of right now
		-- a negative value results in the character to the right of the one specified 
		-- being further away.	

		offset = {
			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,
			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,
			0,0,0,0,0,-1,0,0, -- make period stand out more by adding spacing
			0,0,0,0,0,0,0,0,
			0,0,0,0,0,0,0,0,
			0,0,-1,0,0,0,0,0,
		},

		-- Define special characters
		-- space maps to " "
		-- Undefined is what is shown when no sprite is mapped to a character.
		-- Undefined is the only required property for fonts.

		special = {
			space = --[[pod_type="gfx"]]unpod("b64:bHo0AAwAAAALAAAAsHB4dQBDIAUJBPAd"),
			undefined = --[[pod_type="gfx"]]unpod("b64:bHo0ABwAAAAaAAAA8AtweHUAQyAFCAQHIAcAJwA3ACcAJwA3AIcAFw==")
		}
}

simple_font = Font:new(simple)

Then text can be drawn using fonts in the draw loop like:

-- font:draw(text, x, y, scale, color, {
--     kerning = int?,
--     line_spacing = int?,
--     wrap = {
--         enabled = bool?,
--         wrap_bounds = {x = int, y = int, w = int, h = int}?,
--         wrap_offscreen = bool?,
--     }?
-- })
simple_font:draw("Hello world!", 2, 2, 1, 7)

Full code

-- 	CONSTANTS {
		sw = 480
		sh = 270
-- 	}

Font = {}
Font.__index = Font

-- Constructor method to initialize the font object
function Font:new(f)
    local obj = {}
    setmetatable(obj, Font)

    if not f.special.undefined then
        error("Fonts must specify at least an undefined character.")
    end

    obj.font_data = self:parseFont(f)

    return obj
end

-- Method to parse the font structure
function Font:parseFont(f)
        local cc = ""

        -- Turn a char array (2D) into a single long string.
        for i = 1, #f.chars do
            if type(f.chars[i]) ~= "string" then
                error(string.format(
                    "2D string array has non-string type (expected: 'string', got '%s') at c[%s]",
                    type(f.chars[i]), i
                ))
            end
            cc = cc .. f.chars[i]
        end

    local font_chars = cc
    local char_data = {
        undefined = {
        	bmp = f.special.undefined,
        	width = f.special.undefined:width(),
        	height = f.special.undefined:height(),
        	ascent = 0,
        	offset = 0,
        },
        space = {
      		bmp = f.special.space or f.special.undefined,
      		width = (f.special.space or f.special.undefined):width(),
      		height = (f.special.space or f.special.undefined):height(),
			ascent = 0,
        	offset = 0,
        },
    }

    -- Loop through every font_char character
    for i = 1, #font_chars do
        local c = font_chars:sub(i, i) -- Lua indexing with sub for characters
        if c ~= " " then
            -- Save character info
            char_data[c] = {
                bmp = f.pod[i].bmp,
                width = f.pod[i].bmp:width(),
                height = f.pod[i].bmp:height(),
                ascent = f.ascent[i],
                offset = f.offset[i],
            }
        end
    end

    return {
        meta = {
            name = f.meta.name or "No name",
            author = f.meta.author or "No author"
        },
        chars = char_data
    }
end

function Font:character(c,col)
	col = col or 7
	if c == " " then
		return self.font_data.chars.space
	elseif c == "\n" then
		return {
			special = "newline",
			width = 0,
			height = 0
		}
	elseif not self.font_data.chars[c] then
		return self.font_data.chars.undefined
	else
		return self.font_data.chars[c]
	end
end

function Font:chars(s) --col)
	local c = {}
	-- s: string
	for i=1,#s do
		table.insert(c, self:character(s[i]))
	end
	return c
end

function Font:recolored(col)

	local function recolor_chars(nc)
		local c = self.font_data.chars

		for k,v in pairs(c) do

			-- CHANGE THIS 7 TO THE COLOR THE FONT IS IN IN THE SPRITE EDITOR
			c[k] = v
			c[k].bmp = replaceAll(c[k].bmp, 7, nc)
		end

		return c
	end

	-- Cache the recolered versions of the font.
	self.cache = self.cache or {}
	self.cache.colored = self.cache.colored or {}
	self.cache.colored[col] = self.cache.colored[col] or recolor_chars(col)

	return self.cache.colored[col]
end

local function ud_replaceAll(ud, original, new)
	local w = ud:width()
	local h = ud:height()

	local n = ud:copy()

	for y=0,h-1 do
		for x=0,w-1 do
			if (n:get(x,y) == original) n:set(x,y,new)
		end
	end

	return n
end

local function renderChars(x, y, chars, opts)
	-- Set default values for opts if not provided
	opts = opts or {}
	opts.size = opts.size or 1
	opts.color = opts.color or 7
	opts.kerning = opts.kerning or 1
	opts.line_spacing = opts.line_spacing or 2
	opts.wrap = opts.wrap or {
		enabled = false,
		wrap_bounds = {x = 0, y = 0, w = 100, h = 100},
		wrap_offscreen = true
	}

	local xo = 0
	local yo = 0
	local running_max_height = 0
	local size = opts.size
	local wrap_enabled = opts.wrap.enabled
	local wrap_bounds = opts.wrap.wrap_bounds
	local wrap_offscreen = opts.wrap.wrap_offscreen
	local kerning = opts.kerning
	local line_spacing = opts.line_spacing

	-- Enable clipping if wrap is enabled
	if wrap_enabled then
		clip(wrap_bounds.x, wrap_bounds.y, wrap_bounds.w, wrap_bounds.h)
	end

	for i, v in ipairs(chars) do

		if v.special == "newline" then
			yo =  yo + running_max_height + line_spacing
			xo = 0
			-- Reset the running height for the next line
			running_max_height = 0
		else

			-- Calculate scaled width, height, ascent, and offset
			local scaled_width = v.width * size
			local scaled_height = v.height * size
			local scaled_offset = v.offset * size
			local scaled_ascent = v.ascent * size
			local scaled_kerning = kerning * size

			if scaled_height > running_max_height then
				running_max_height = scaled_height
			end

			local recolored = ud_replaceAll(v.bmp, 7, opts.color)

			-- Check for normal wrapping if enabled
			if (x + xo >= wrap_bounds.w - scaled_width) and wrap_enabled then
				yo = yo + running_max_height + line_spacing
				xo = 0
				-- Reset the running height for the next line
				running_max_height = 0
			end

			-- Check for offscreen wrapping if enabled
			if wrap_offscreen and (x + xo + scaled_width >= sw) then
				yo = yo + running_max_height + line_spacing
				xo = 0
				running_max_height = 0
			end

			-- Draw the sprite with consistent scaling
			sspr(recolored, 0, 0, v.width, v.height, x + xo, y + (yo - scaled_ascent), scaled_width, scaled_height)

			-- Update x-offset by width, scaled kerning, and offset
			xo = xo + (scaled_width - scaled_offset + scaled_kerning)
		end
	end

	-- Reset the clipping bounds
	clip()
end

function Font:draw(text, x, y, size, col, opts)
	-- Calculate the recolored font.
	--self:recolored(col)

	opts = opts or {}
	opts.size = size	
	opts.color = 	col or 7

	-- Resolve the characters for text
	local chars = self:chars(text,col)

	-- Render the text
	renderChars(x,y,chars,opts)
end

Coming soon

  • PicoUI integration with adaptive, sizable view
1



[Please log in to post a comment]