Defold-RichText

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.

9 Likes