Typewriting Effect in Defold

Hello,

I’m learning Defold and want texts to show one by one like typewriting.

I’ve found “Defold Typewriter” in the assets section and tried it.
But it caused errors saying Lua 5.1 does not support this grammar. (Supported in Lua 5.2….)
Could you anyone please explain how to fix this problem?

1 Like

The goto keyword and continue keywords are not supported in Lua 5.1 which we use for HTML5 builds

2 Likes

I fixed this last year if you want to use my fix.

Just save it as lua module and import it and use it the same as the docs say.

--------------------------------------------------------------------------------
-- LICENSE
--------------------------------------------------------------------------------

-- Copyright (c) 2024 White Box Dev

-- This software is provided 'as-is', without any express or implied warranty.
-- In no event will the authors be held liable for any damages arising from the use of this software.

-- Permission is granted to anyone to use this software for any purpose,
-- including commercial applications, and to alter it and redistribute it freely,
-- subject to the following restrictions:

-- 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
--    If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.

-- 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.

-- 3. This notice may not be removed or altered from any source distribution.

--------------------------------------------------------------------------------
-- INFORMATION
--------------------------------------------------------------------------------

-- https://github.com/whiteboxdev/library-defold-typewriter

----------------------------------------------------------------------
-- PROPERTIES
----------------------------------------------------------------------

local dtypewriter = {}

local _container_node

local _font_id
local _font

local _text_area_x
local _text_area_y
local _text_area_width

local _line_count_max
local _line_offset

local _chunks = {}

local _characters = {}
local _character_index
local _paragraph_index

local _waiting = false
local _skip = false

local _colors = {}
local _default_color = vmath.vector4(1, 1, 1, 1)

local _fade_delay = 0

local _default_type_speed = 30

local _messages_url

local _callback_handle

----------------------------------------------------------------------
-- CONSTANTS
----------------------------------------------------------------------

dtypewriter.messages =
{
	start = hash("start"),
	restart = hash("restart"),
	type = hash("type"),
	wait = hash("wait"),
	continue = hash("continue"),
	complete = hash("complete"),
	clear = hash("clear")
}

----------------------------------------------------------------------
-- LOCAL FUNCTIONS
----------------------------------------------------------------------

local function strip_spaces(text)
	local remove_consecutive_spaces = string.gsub(text, "%s+", " ")
	local remove_front_spaces = string.gsub(remove_consecutive_spaces, "^%s+", "")
	local remove_back_spaces = string.gsub(remove_front_spaces, "%s+$", "")
	local remove_line_spaces = string.gsub(remove_back_spaces, "%s*<line>%s*", "<line>")
	return remove_line_spaces
end

local function add_chunk(type, data)
	local chunk = { type = type, data = data or {} }
	_chunks[#_chunks + 1] = chunk
end

local function add_character(chunk_index, text, color, speed)
	local character_data = { chunk_index = chunk_index, text = text, color = color, speed = speed }
	_characters[#_characters + 1] = character_data
end

local function set_transparent(color)
	return vmath.vector4(color.x, color.y, color.z, 0)
end


local function type_callback()
    -- If we haven't processed this character yet, post the "type" message
    if _character_index == 1 or not _waiting then
        msg.post(_messages_url, dtypewriter.messages.type)
    end

    local character_data = _characters[_character_index]

    if _fade_delay > 0 then
        gui.animate(character_data.node, "color.w", 1, gui.EASING_LINEAR, _fade_delay)
    else
        gui.set_color(character_data.node, character_data.color)
    end

    _character_index = _character_index + 1
    local next_character_data = _characters[_character_index]

    if next_character_data then
        if character_data.paragraph < next_character_data.paragraph then
            msg.post(_messages_url, dtypewriter.messages.wait)
            _waiting = true
        elseif next_character_data.speed == 0 or _skip then
            -- Skip the delay and continue immediately
            if not _waiting then
                type_callback()
            end
        else
            _callback_handle = timer.delay(1 / next_character_data.speed, false, type_callback)
        end
    else
        msg.post(_messages_url, dtypewriter.messages.complete)
        _callback_handle = nil
    end
end



----------------------------------------------------------------------
-- MODULE FUNCTIONS
----------------------------------------------------------------------

function dtypewriter.init(container_node_id, font_id, text_area_x, text_area_y, text_area_width, line_count_max, line_offset, messages_url)
	_container_node = gui.get_node(container_node_id)
	_font_id = font_id
	_font = gui.get_font_resource(font_id)
	_text_area_x = text_area_x
	_text_area_y = text_area_y
	_text_area_width = text_area_width
	_line_count_max = line_count_max
	_line_offset = line_offset
	_messages_url = messages_url
end

function dtypewriter.clear()
	msg.post(_messages_url, dtypewriter.messages.clear)
	_text_raw = nil
	_chunks = {}
	for _, character_data in ipairs(_characters) do
		gui.delete_node(character_data.node)
	end
	_characters = {}
	_character_index = nil
	_paragraph_index = nil
	_waiting = false
	if _callback_handle then
		timer.cancel(_callback_handle)
		_callback_handle = nil
	end
end


function dtypewriter.load(text)
    dtypewriter.clear()
    text = strip_spaces(text)
    local chunk_start_index = 1
    local character_index = 1
    local character_color = _default_color
    local character_speed = _default_type_speed

    while character_index <= #text do
        local character = string.sub(text, character_index, character_index)
        if character == " " then
            local chunk_type = "content"
            local chunk_text = string.sub(text, chunk_start_index, character_index - 1)
            local chunk_data = { text = chunk_text, metrics = resource.get_text_metrics(_font, chunk_text) }
            add_chunk(chunk_type, chunk_data)
            chunk_type = "space"
            chunk_text = " "
            chunk_data = { text = chunk_text, metrics = resource.get_text_metrics(_font, chunk_text) }
            add_chunk(chunk_type, chunk_data)
            add_character(#_chunks, chunk_text, character_color, character_speed)
            chunk_start_index = character_index + 1
            character_index = character_index + 1
        elseif character == "<" then
            if string.sub(text, character_index, character_index + 6) == "<color=" then
                local color_start_index, color_end_index = string.find(text, "%l+", character_index + 7)
                local color_name = string.sub(text, color_start_index, color_end_index)
                character_color = (color_name == "default" or not _colors[color_name]) and _default_color or _colors[color_name]
                text = string.sub(text, 1, character_index - 1) .. string.sub(text, color_end_index + 2)
            elseif string.sub(text, character_index, character_index + 6) == "<speed=" then
                local speed_start_index, speed_end_index = string.find(text, "%d*%l*", character_index + 7)
                local speed_text = string.sub(text, speed_start_index, speed_end_index)
                if speed_text == "default" then
                    character_speed = _default_type_speed
                elseif speed_text == "instant" then
                    character_speed = 0
                else
                    character_speed = speed_text
                end
                text = string.sub(text, 1, character_index - 1) .. string.sub(text, speed_end_index + 2)
            elseif string.sub(text, character_index, character_index + 5) == "<line>" then
                local chunk_type = "content"
                local chunk_text = string.sub(text, chunk_start_index, character_index - 1)
                local chunk_data = { text = chunk_text, metrics = resource.get_text_metrics(_font, chunk_text) }
                add_chunk(chunk_type, chunk_data)
                chunk_type = "line"
                add_chunk(chunk_type)
                chunk_start_index = character_index + 6
                character_index = character_index + 6
            elseif string.sub(text, character_index, character_index + 10) == "<paragraph>" then
                local chunk_type = "content"
                local chunk_text = string.sub(text, chunk_start_index, character_index - 1)
                local chunk_data = { text = chunk_text, metrics = resource.get_text_metrics(_font, chunk_text) }
                add_chunk(chunk_type, chunk_data)
                chunk_type = "paragraph"
                add_chunk(chunk_type)
                chunk_start_index = character_index + 11
                character_index = character_index + 11
            end
        else
            add_character(#_chunks + 1, character, character_color, character_speed)
            if character_index == #text then
                local chunk_type = "content"
                local chunk_text = string.sub(text, chunk_start_index, character_index)
                local chunk_data = { text = chunk_text, metrics = resource.get_text_metrics(_font, chunk_text) }
                add_chunk(chunk_type, chunk_data)
            end
            character_index = character_index + 1
        end
    end

    local text_metrics = resource.get_text_metrics(_font, text)
    local paragraph = 1
    local line = 1
    local line_width_remaining = _text_area_width
    local character_x = _text_area_x
    local invalid_chunk_indices = {}

    for chunk_index, chunk in ipairs(_chunks) do
        if chunk.type == "paragraph" then
            line = 1
            line_width_remaining = _text_area_width
            paragraph = paragraph + 1
            character_x = _text_area_x
        elseif chunk.type == "line" or (chunk.data.text and line_width_remaining - chunk.data.metrics.width < 0) then
            line = line + 1
            line_width_remaining = _text_area_width
            if line > _line_count_max then
                line = 1
                paragraph = paragraph + 1
            end
            character_x = _text_area_x
            if chunk.type == "space" then
                -- Mark this chunk as invalid (to skip processing it)
                invalid_chunk_indices[chunk_index] = true
            end
        end

        if chunk.data.text then
            for _, character_data in ipairs(_characters) do
                if character_data.chunk_index == chunk_index then
                    local character_metrics = resource.get_text_metrics(_font, character_data.text)
                    local character_position = vmath.vector3(character_x, -_text_area_y - (line - 1) * text_metrics.height + (line - 1) * _line_offset, 0)
                    character_x = character_x + character_metrics.width + 1
                    line_width_remaining = line_width_remaining - character_metrics.width - 1
                    local character_node = gui.new_text_node(character_position, character_data.text)
                    gui.set_parent(character_node, _container_node)
                    gui.set_font(character_node, _font_id)
                    gui.set_color(character_node, set_transparent(character_data.color))
                    gui.set_adjust_mode(character_node, gui.ADJUST_FIT)
                    gui.set_pivot(character_node, gui.PIVOT_NW)
                    character_data.node = character_node
                    character_data.paragraph = paragraph
                end
            end
        end
    end

    -- Now remove invalid chunks (spaces)
    for character_index = #_characters, 1, -1 do
        if invalid_chunk_indices[_characters[character_index].chunk_index] then
            table.remove(_characters, character_index)
        end
    end
end


function dtypewriter.start()
	if dtypewriter.is_loaded() then
		msg.post(_messages_url, dtypewriter.messages.start)
		_character_index = 1
		_paragraph_index = 1
		_callback_handle = timer.delay(0, false, type_callback)
	end
end

function dtypewriter.restart()
	if dtypewriter.is_typing() or dtypewriter.is_waiting() or dtypewriter.is_complete() then
		msg.post(_messages_url, dtypewriter.messages.restart)
		_character_index = 1
		_paragraph_index = 1
		_waiting = false
		if _callback_handle then
			timer.cancel(_callback_handle)
		end
		for character_index = 1, #_characters do
			local character_data = _characters[character_index]
			gui.set_color(character_data.node, set_transparent(character_data.color))
		end
		_callback_handle = timer.delay(0, false, type_callback)
	end
end

function dtypewriter.continue()
	if _waiting then
		msg.post(_messages_url, dtypewriter.messages.continue)
		_waiting = false
		_paragraph_index = _paragraph_index + 1
		character_index = 1
		while character_index < _character_index do
			local character_data = _characters[character_index]
			gui.set_color(character_data.node, set_transparent(character_data.color))
			character_index = character_index + 1
		end
		_callback_handle = timer.delay(0, false, type_callback)
	end
end

function dtypewriter.skip()
	if dtypewriter.is_typing() then
		if _callback_handle then
			timer.cancel(_callback_handle)
		end
		_skip = true
		type_callback()
		_skip = false
	end
end

function dtypewriter.add_color(name, color)
	_colors[name] = color
end

function dtypewriter.set_default_color(color)
	_default_color = color
end

function dtypewriter.clear_colors()
	_colors = {}
end

function dtypewriter.set_fade_delay(delay)
	_fade_delay = delay
end

function dtypewriter.set_default_type_speed(speed)
	_default_type_speed = speed
end

function dtypewriter.is_clear()
	return not _character_index and #_characters == 0
end

function dtypewriter.is_loaded()
	return not _character_index and #_characters > 0
end

function dtypewriter.is_typing()
	return _character_index and _character_index <= #_characters and not _waiting
end

function dtypewriter.is_waiting()
	return _waiting
end

function dtypewriter.is_complete()
	return _character_index and _character_index == #_characters + 1
end

return dtypewriter

Thank you, “goeshard”!

The lua file is working perfectly. I appreciate your help.