I wanted to achieve text effects like in Celeste and Slay the Spire. I took the code from the “Characters” example in the main Richtext project and wrote a couple of effects functions. I then wrapped richtext.create in a new function that automatically calls these effect functions (richtext.create_with_animations). It’s dirty and probably not optimised, but it works for me and might be useful for someone else.
This is how it looks:
These are the tags, in order of their appearance in the clip:
shakeletters
waveletters
wavewords
shakewords
Use them like this:
<shakeletters>shake each letter!</shakeletters>
The code:
function richtext.wave_letters(words)
local waves = richtext.tagged(words, "waveletters")
for _,wave in pairs(waves) do
for i,word in ipairs(words) do
if word == wave then
table.remove(words, i)
break
end
end
gui.delete_node(wave.node)
local chars = richtext.characters(wave)
for i,char in ipairs(chars) do
local pos = gui.get_position(char.node)
local pos_2 = gui.get_position(char.node)
pos_2.y = pos_2.y - 1.5
pos_2.x = pos_2.x + 0
gui.set_position(char.node, pos_2)
local amplitude = tonumber(wave.tags.wave) or 1.5
gui.animate(char.node, gui.PROP_POSITION, pos + vmath.vector3(0, amplitude, 0), gui.EASING_INOUTSINE, 1.2, i * 0.12112, nil, gui.PLAYBACK_LOOP_PINGPONG)
table.insert(words, char)
end
end
end
function richtext.shake_letters(words)
local waves = richtext.tagged(words, "shakeletters")
for _,wave in pairs(waves) do
for i,word in ipairs(words) do
if word == wave then
table.remove(words, i)
break
end
end
gui.delete_node(wave.node)
local chars = richtext.characters(wave)
for i,char in ipairs(chars) do
local pos = gui.get_position(char.node)
local pos_2 = gui.get_position(char.node)
pos_2.y = pos_2.y - 0.75
pos_2.x = pos_2.x - 0.75
gui.set_position(char.node, pos_2)
gui.animate(char.node, "position.x", pos.x + 0.75 * math.random(), gui.EASING_INOUTBOUNCE, 0.1+0.05 * math.random(), i * 0.12112, nil, gui.PLAYBACK_LOOP_PINGPONG)
gui.animate(char.node, "position.y", pos.y + 0.75 * math.random(), gui.EASING_INOUTBOUNCE, 0.1+0.05 * math.random(), i * 0.12112, nil, gui.PLAYBACK_LOOP_PINGPONG)
table.insert(words, char)
end
end
end
function richtext.wave_words(words)
local waves = richtext.tagged(words, "wavewords")
local i = 0
for _,wave in pairs(waves) do
i = i + 1
local pos = gui.get_position(wave.node)
gui.set_position(wave.node, pos - vmath.vector3(0,1.5,0))
gui.animate(wave.node, gui.PROP_POSITION, pos + vmath.vector3(0, 1.5, 0), gui.EASING_INOUTSINE, 1.2, i * 0.12112, nil, gui.PLAYBACK_LOOP_PINGPONG)
end
end
function richtext.shake_words(words)
local waves = richtext.tagged(words, "shakewords")
local i = 0
for _,wave in pairs(waves) do
i = i + 1
local pos = gui.get_position(wave.node)
gui.set_position(wave.node, pos - vmath.vector3(1,1,0))
gui.animate(wave.node, "position.x", pos.x + 1, gui.EASING_INOUTBOUNCE, 0.075+0.05*math.random(), i * 0.12112, nil, gui.PLAYBACK_LOOP_PINGPONG)
gui.animate(wave.node, "position.y", pos.y + 1, gui.EASING_INOUTBOUNCE, 0.075+0.05*math.random(), i * 0.12112, nil, gui.PLAYBACK_LOOP_PINGPONG)
end
end
function richtext.create_with_animations(text, font, settings)
local words, metrics = richtext.create(text, font, settings)
richtext.wave_letters(words)
richtext.shake_letters(words)
richtext.wave_words(words)
richtext.shake_words(words)
return words, metrics
end
If you find this useful I encourage you to write your own effect functions! I think the main use of my example is to illustrate a) how to wrap richtext.create in a new function which applies all of your effects, b) how to animate each tagged word and c) how to animate each tagged letter.
Very happy with this extension, thanks @britzl.