-- A generic text object. -- It implements a character based tagging system which should allow you to implement any kind of text effect possible, from setting a character's color to making it become visible, shake and play sounds. -- You would use it like this: --[[ yellow_text_tag = TextTag({ init = function(c, i, text) text.yellow = Color(1, 0.5, 0, 1) end, draw = function(c, i, text) graphics.set_color(text.yellow) end }) shaking_text_tag = TextTag({ init = function(c, i, text) c.shaking_intensity = 8 end, update = function(c, dt, i, text) c.ox = random:float(-c.shaking_intensity, c.shaking_intensity) c.oy = random:float(-c.shaking_intensity, c.shaking_intensity) end }) text = Text({ {text: '[yellow]This text is yellow', font: some_font, alignment: 'center', height_offset: -10} {text: '[shaking]This text is shaking', font: some_other_font, alignment: 'center', height_multiplier: 1.2} {text: 'This text is normal', font: yet_another_font, alignment: 'center', height_multiplier: 1.2} {text: '[yellow, shaking]This text is yellow and shaking []while this text is normal', font: some_font, alignment: 'center'} }, {yellow = yellow_text_tag, shaking = shaking_text_tag}) ]]-- -- There are two main things happening in the example above: first we're creating TextTags and then we're creating a text object that uses those tags. -- The way each tag works is fairly simple: a tag accepts 3 functions, init, update and draw, and each of those functions operates on the text's characters one at a time. -- In the example above, the text without tags is 'This text is yellow while this text is shaking and this text is normal' -- For each of the characters in that string, different functions will be applied based on what tags were previously applied to it. -- init, update and draw functions take in 3 arguments in common: -- c - the character in question, a table containing .x, .y, .r, .sx, .sy, .ox, .oy and .character attributes. -- i - the index of character in the string -- text - the reference to the text object -- The update function also takes in dt as the second argument. -- -- After we're done creating TextTags, we have to create the actual text object. -- The way this is done is by specifying each line of the text object, along with its font, alignment, height multipliers and height offsets. -- The first argument (text_data) is a table of tables containing all relevant info: -- text - the actual string containing the text to be displayed, along with any tag information -- font - the font to be used for the text -- alignment (optional) - how the text should align itself, possible values are 'center', 'justified', 'right', if not specified then by default it's 'left' -- height_offset (optional) - how many pixels the line below this one should be offset by -- height_multiplier (optional) - multiplier over the font's height for placing the line below -- The text object itself also has .w and .h which corresponds to the width of the biggest line and height of all lines + offsets, respectively. -- If 'alignment_width' is set to a specific line then that line will be automatically set to that width, and if it is the biggest then .w will also be set to that value. Text = Object:extend() function Text:init(text_data, text_tags) self.t = Trigger() self.text_data = text_data self.text_tags = text_tags self.white = Color(1, 1, 1, 1) self:set_text(text_data) return self end function Text:update(dt) self.t:update(dt) self:format_text() for _, line in ipairs(self.lines) do for i, c in ipairs(line.characters) do for k, v in pairs(self.text_tags) do for _, tag in ipairs(c.tags) do if tag == k then if v.actions.update then v.actions.update(c, dt, i, self) end end end end end end end -- Draws the text object centered at the specified location. function Text:draw(x, y, r, sx, sy) for _, line in ipairs(self.lines) do for i, c in ipairs(line.characters) do for k, v in pairs(self.text_tags) do for _, tag in ipairs(c.tags) do if tag == k then if v.actions.draw then v.actions.draw(c, i, self) end end end end graphics.push(x, y, r, sx, sy) graphics.print(c.character, line.font, x + c.x - self.w/2, y + c.y - self.h/2, c.r or 0, c.sx or 1, c.sy or c.sx or 1, c.ox or 0, c.oy or 0) graphics.pop() graphics.set_color(self.white) end end end function Text:format_text() self.w = 0 for i, line in ipairs(self.lines) do local line_width = math.max(line.font:get_text_width(line.raw_text), line.alignment_width or 0) if line_width > self.w then self.w = line_width end end local x, y = 0, 0 for j, line in ipairs(self.lines) do local h = (line.font.h*(line.height_multiplier or 1) + (line.height_offset or 0))*(line.sy or 1) for i, c in ipairs(line.characters) do c.x = x c.y = y c.sx = line.sx or c.sx or 1 c.sy = line.sy or c.sy or 1 x = x + line.font:get_text_width(c.character) end y = y + h x = 0 end self.h = y for i, line in ipairs(self.lines) do if line.alignment == "right" then local text_width = 0 for _, c in ipairs(line.characters) do text_width = text_width + line.font:get_text_width(c.character) end local left_over_width = self.w - (line.alignment_width or text_width) for _, c in ipairs(line.characters) do c.x = c.x + left_over_width end elseif line.alignment == "center" then local text_width = 0 for _, c in ipairs(line.characters) do text_width = text_width + line.font:get_text_width(c.character) end local left_over_width = self.w - (line.alignment_width or text_width) for _, c in ipairs(line.characters) do c.x = c.x + left_over_width/2 end elseif line.alignment == "justified" then local text_width = 0 for _, c in ipairs(line.characters) do text_width = text_width + line.font:get_text_width(c.character) end local left_over_width = self.w - (line.alignment_width or text_width) local spaces_count = 0 for _, c in ipairs(line.characters) do if c.character == " " then spaces_count = spaces_count + 1 end end local added_width_to_each_space = math.floor(left_over_width/spaces_count) local total_added_width = 0 for _, c in ipairs(characters) do if c.character == " " then c.x = c.x + added_width_to_each_space total_added_width = total_added_width + added_width_to_each_space else c.x = c.x + total_added_width end end end end end function Text:parse(text_data) for _, line in ipairs(text_data) do local tags = {} for i, tags_text, j in line.text:gmatch("()%[(.-)%]()") do if tags_text == "" then table.insert(tags, {i = tonumber(i), j = tonumber(j)-1}) line.tags = tags else local local_tags = {} for tag in tags_text:gmatch("[%w_]+") do table.insert(local_tags, tag) end table.insert(tags, {i = tonumber(i), j = tonumber(j)-1, tags = local_tags}) line.tags = tags end end if not line.tags then line.tags = {} end end for _, line in ipairs(text_data) do line.characters = {} local current_tags = nil for i = 1, #line.text do local c = line.text:sub(i, i) local inside_tags = false for _, tag in ipairs(line.tags) do if i >= tag.i and i <= tag.j then inside_tags = true current_tags = tag.tags break end end if not inside_tags then table.insert(line.characters, {character = c, visible = true, tags = current_tags or {}}) end end end for _, line in ipairs(text_data) do local raw_text = "" for _, character in ipairs(line.characters) do raw_text = raw_text .. character.character end line.raw_text = raw_text end return text_data end -- Sets new text. -- Reapplies all modifications (wrap width, justification, etc). function Text:set_text(text_data) self.lines = self:parse(text_data) self:format_text() for _, line in ipairs(self.lines) do for i, c in ipairs(line.characters) do for k, v in pairs(self.text_tags) do for _, tag in ipairs(c.tags) do if tag == k then if v.actions.init then v.actions.init(c, i, self) end end end end end end end -- Sets the line's alignment width. -- This is used to align the text according to the alignment option -- For instance, if the alignment width is 200 and the alignment is 'right', then the right edge used for this alignment will be 200 units to the right function Text:set_alignment_width(line, alignment_width) self.alignment_width = alignment_width self:format_text() return self end -- Sets the text's line height. -- Lines are automatically placed vertically using the font's height for spacing, but you can increase or decrease this distance by setting these values. function Text:set_line_height_data(line, offset, multiplier) self.lines[line].height_offset = offset or 0 self.lines[line].height_multiplier = multiplier or 1 self:format_text() return self end -- Sets the text's font. By default texts use the global font. function Text:set_font(line, font) self.lines[line].font = font self:format_text() return self end -- Sets the alignment behavior for the given line. -- Possible behaviors are: 'right', 'center' and 'justified' function Text:set_alignment(line, alignment) self.lines[line].alignment = alignment self:format_text() return self end -- The text tag objects to be used with text instances. TextTag = Object:extend() function TextTag:init(actions) self.actions = actions end