diff --git a/README.md b/README.md
index f11853b..ac6d8b1 100644
--- a/README.md
+++ b/README.md
@@ -42,8 +42,9 @@ The following tags are supported:
| i | The text should be italic | `Foobar` |
| size | Change text size, relative to default size | `Twice as large` |
| color | Change text color | `Foobar` |
-| | | `Foobar` |
-| | | `Foobar` |
+| | | `Foobar` |
+| | | `Foobar` |
+| | | `Foobar` |
| font | Change font | `Foobar` |
| img | Display image | `` |
| br | Insert a line break (see notes on linebreak) | `
` |
@@ -79,6 +80,7 @@ The following named colors are supported:
| white | `#ffffffff` | ![](https://placehold.it/15/ffffff/000000?text=+) |
| yellow | `#ffff00ff` | ![](https://placehold.it/15/ffff00/000000?text=+) |
+
# Usage
The RichText library will create gui text nodes representing the markup in the text passed to the library. It will search for tags and split the entire text into words, where each word contains additional meta-data that is used to create and configure text nodes. This means that the library will create as many text nodes as there are words in the text.
@@ -116,7 +118,6 @@ A more complex example with different fonts, colors, inline images and automatic
![](docs/example.png)
-
# API
### richtext.create(text, font, settings)
Creates rich text gui nodes from a text containing markup.
@@ -174,6 +175,16 @@ Get the length of a text ignoring any tags except image tags which are treated a
* `length` (number) - The length of the provided text.
+### richtext.characters(word)
+Split a word into it's characters, including the creation of the gui nodes. Each of the characters will be given the same attributes as the word, and they will be positioned correctly within the word.
+
+**PARAMETERS**
+* `word` (table) - The word to split, as received from a call to `richtext.create()` or `richtext.tagged()`.
+
+**RETURNS**
+* `characters` (table) - The individual characters of the word.
+
+
### richtext.ALIGN_LEFT
Left-align text. The words of a line starts at the specified position (see `richtext.create` settings above).
diff --git a/example/example.gui_script b/example/example.gui_script
index a8116c6..4db3112 100644
--- a/example/example.gui_script
+++ b/example/example.gui_script
@@ -51,7 +51,6 @@ end
local function create_truncate_example()
local settings = { position = vmath.vector3(0, 260, 0) }
local words, metrics = richtext.create("This text should be shown one at a time...", "Roboto-Regular", settings)
- pprint(metrics)
local length = 0
richtext.truncate(words, length)
timer.repeating(0.1, function()
@@ -61,8 +60,27 @@ local function create_truncate_example()
end
+
+local function create_characters_example()
+ local settings = { position = vmath.vector3(0, 330, 0) }
+ local words, metrics = richtext.create("Our princess is in another castle", "Roboto-Regular", settings)
+
+ local waves = richtext.tagged(words, "wave")
+ for _,wave in pairs(waves) do
+ local chars = richtext.characters(wave)
+ gui.delete_node(wave.node)
+ for i,char in ipairs(chars) do
+ local pos = gui.get_position(char.node)
+ gui.animate(char.node, gui.PROP_POSITION, pos + vmath.vector3(0, 15, 0), gui.EASING_INOUTSINE, 2, i * 0.3, nil, gui.PLAYBACK_LOOP_PINGPONG)
+ end
+ end
+end
+
+
+
function init(self)
create_complex_example()
create_align_example()
create_truncate_example()
+ create_characters_example()
end
diff --git a/richtext/parse.lua b/richtext/parse.lua
index 10c388d..5e5ca29 100644
--- a/richtext/parse.lua
+++ b/richtext/parse.lua
@@ -27,6 +27,7 @@ local function parse_tag(tag, params)
return settings
end
+
-- add a single word to the list of words
local function add_word(text, settings, words)
local data = { text = text }
@@ -36,6 +37,7 @@ local function add_word(text, settings, words)
words[#words + 1] = data
end
+
-- split a line into words
local function split_line(line, settings, words)
assert(line)
@@ -56,6 +58,7 @@ local function split_line(line, settings, words)
end
end
+
-- split text
-- split by lines first
local function split_text(text, settings, words)
@@ -91,6 +94,7 @@ local function split_text(text, settings, words)
end
end
+
-- find tag in text
-- return the tag, tag params and any text before and after the tag
local function find_tag(text)
@@ -121,6 +125,11 @@ local function find_tag(text)
end
end
+
+--- Parse the text into individual words
+-- @param text The text to parse
+-- @param word_settings Default settings for each word
+-- @return List of all words
function M.parse(text, word_settings)
assert(text)
assert(word_settings)
@@ -161,6 +170,7 @@ end
--- Get the length of a text, excluding any tags (except image tags)
function M.length(text)
+ -- treat image tags as a single character
return #text:gsub("", " "):gsub("<.->", "")
end
diff --git a/richtext/richtext.lua b/richtext/richtext.lua
index d7f346c..6168bfa 100644
--- a/richtext/richtext.lua
+++ b/richtext/richtext.lua
@@ -10,11 +10,31 @@ M.ALIGN_RIGHT = hash("ALIGN_RIGHT")
local V3_ZERO = vmath.vector3(0)
local V3_ONE = vmath.vector3(1)
+function deepcopy(orig)
+ local orig_type = type(orig)
+ local copy
+ if orig_type == 'table' then
+ copy = {}
+ for orig_key, orig_value in next, orig, nil do
+ copy[deepcopy(orig_key)] = deepcopy(orig_value)
+ end
+ else -- number, string, boolean, etc
+ copy = orig
+ end
+ return copy
+end
+
+
local function get_trailing_whitespace(text)
return text:match("^.-(%s*)$") or ""
end
+local function get_space_width(font)
+ return gui.get_text_metrics(font, " _").width - gui.get_text_metrics(font, "_").width
+end
+
+
local function get_font(word, fonts)
local font_settings = fonts[word.font]
local font = nil
@@ -76,6 +96,58 @@ function M.length(text)
end
+local function create_box_node(word)
+ local node = gui.new_box_node(V3_ZERO, V3_ZERO)
+ gui.set_size_mode(node, gui.SIZE_MODE_AUTO)
+ gui.set_texture(node, word.image.texture)
+ gui.play_flipbook(node, hash(word.image.anim))
+
+ -- get metrics of node based on image size
+ local image_size = gui.get_size(node)
+ local metrics = {}
+ metrics.total_width = image_size.x
+ metrics.width = image_size.x
+ metrics.height = image_size.y
+ return node, metrics
+end
+
+
+local function create_text_node(word, font, font_cache)
+ local node = gui.new_text_node(V3_ZERO, word.text)
+ gui.set_font(node, font)
+ gui.set_color(node, word.color)
+ gui.set_scale(node, V3_ONE * (word.size or 1))
+
+ -- get metrics of node with and without trailing whitespace
+ local metrics = gui.get_text_metrics(font, word.text)
+ metrics.width = metrics.width * word.size
+ metrics.height = metrics.height * word.size
+
+ -- get width of text with trailing whitespace included
+ local trailing_whitespace = get_trailing_whitespace(word.text)
+ if #trailing_whitespace > 0 then
+ metrics.total_width = (metrics.width + #trailing_whitespace * get_space_width(font)) * word.size
+ else
+ metrics.total_width = metrics.width
+ end
+ return node, metrics
+end
+
+
+local function create_node(word, parent, font)
+ local node, metrics
+ if word.image then
+ node, metrics = create_box_node(word)
+ else
+ node, metrics = create_text_node(word, font)
+ end
+ gui.set_pivot(node, gui.PIVOT_NW)
+ gui.set_parent(node, parent)
+ return node, metrics
+end
+
+
+
--- Create rich text gui nodes from text
-- @param text The text to create rich text nodes from
-- @param font The default font
@@ -92,9 +164,6 @@ function M.create(text, font, settings)
settings.color = settings.color or V3_ONE
settings.position = settings.position or V3_ZERO
- -- cache length fonr metrics such as space width and height
- local font_sizes = {}
-
-- default settings for a word
-- will be assigned to each word unless tags override the values
local word_settings = {
@@ -118,47 +187,9 @@ function M.create(text, font, settings)
-- get font to use based on word tags
local font = get_font(word, settings.fonts)
- -- cache some font measurements for the current font
- if not font_sizes[font] then
- font_sizes[font] = {
- space = gui.get_text_metrics(font, " _").width - gui.get_text_metrics(font, "_").width,
- height = gui.get_text_metrics(font, "Ij").height,
- }
- end
- -- create and configure node
- local node
- if word.image then
- node = gui.new_box_node(V3_ZERO, V3_ZERO)
- gui.set_size_mode(node, gui.SIZE_MODE_AUTO)
- gui.set_texture(node, word.image.texture)
- gui.play_flipbook(node, hash(word.image.anim))
-
- -- get metrics of node based on image size
- local image_size = gui.get_size(node)
- word.metrics = {}
- word.metrics.total_width = image_size.x
- word.metrics.width = image_size.x
- word.metrics.height = image_size.y
- else
- node = gui.new_text_node(V3_ZERO, word.text)
- gui.set_font(node, font)
- gui.set_color(node, word.color)
- gui.set_scale(node, V3_ONE * (word.size or 1))
-
- -- get metrics of node with and without trailing whitespace
- word.metrics = gui.get_text_metrics_from_node(node)
- word.metrics.total_width = (word.metrics.width + #get_trailing_whitespace(word.text) * font_sizes[font].space) * word.size
- word.metrics.width = word.metrics.width * word.size
- word.metrics.height = word.metrics.height * word.size
- end
-
- -- store node on word and set it's parent and pivot
- word.node = node
- if settings.parent then
- gui.set_parent(node, settings.parent)
- end
- gui.set_pivot(node, gui.PIVOT_NW)
+ -- create node and get metrics
+ word.node, word.metrics = create_node(word, settings.parent, font)
-- does the word fit on the line or does it overflow?
local overflow = (settings.width and (line_width + word.metrics.width) > settings.width)
@@ -256,4 +287,46 @@ function M.truncate(words, length)
end
+--- Split a word into it's characters
+-- @param word The word to split
+-- @return The individual characters
+function M.characters(word)
+ assert(word)
+
+ -- exit early if word is a single character or empty
+ if #word.text <= 1 then
+ local char = deepcopy(word)
+ char.node, char.metrics = create_node(char, parent, font)
+ ui.set_position(char.node, gui.get_position(word.node))
+ return { char }
+ end
+
+ -- split word into characters
+ local parent = gui.get_parent(word.node)
+ local font = gui.get_font(word.node)
+ local chars = {}
+ local chars_width = 0
+ for i=1,#word.text do
+ local char = deepcopy(word)
+ chars[#chars + 1] = char
+ char.text = word.text:sub(i,i)
+ char.node, char.metrics = create_node(char, parent, font)
+ chars_width = chars_width + char.metrics.width
+ end
+
+ -- position each character
+ -- take into account that the sum of the width of the individual
+ -- characters differ from the width of the entire word
+ local position = gui.get_position(word.node)
+ local spacing = (word.metrics.width - chars_width) / (#chars - 1)
+ for i=1,#chars do
+ local char = chars[i]
+ gui.set_position(char.node, position)
+ position.x = position.x + char.metrics.width + spacing
+ end
+
+ return chars
+end
+
+
return M
\ No newline at end of file