Justified text displaying one character at a time

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
saddleblasters
Prole
Posts: 3
Joined: Tue Jan 31, 2017 11:50 pm

Justified text displaying one character at a time

Post by saddleblasters »

I'm trying to implement the effect you see in many games where text is printed a character at a time. (See the original Dragon Quest for an example.) On its own this is easy: all one needs to do is keep a counter then use string.sub and utf8.offset to print a substring that gets longer and longer. The trouble comes from another limitation I have: I'd like to use printf's "justify" alignment. The naive implementation of this looks weird, as though text is swooping in from the right. The reason for this is clear: the justification changes the position of text that's already printed. I'm not really sure how to solve this problem though, and I wasn't able to find anyone else posting about it.

The only workaround I can think of is to print all the text in one go, properly justified, but have it start out covered by black rectangles over each line, then shrink the rectangles a character at a time until all the text is revealed. This should work, but it seems a bit convoluted and might cause problems later down the line if, say, I want a semi-transparent textbox.

Is there a simpler solution?
Attachments
Dec-11-2023 19-42-09.gif
Dec-11-2023 19-42-09.gif (44.54 KiB) Viewed 49580 times
User avatar
pgimeno
Party member
Posts: 3550
Joined: Sun Oct 18, 2015 2:58 pm

Re: Justified text displaying one character at a time

Post by pgimeno »

Just use left-alignment instead of justification. Even if it worked as you expect, it would have to justify the whole line every time one is completed, and if you're still reading that, it's uncomfortable.

If you absolutely want to try it, you can try printing all lines except the last one justified, and the last one left-aligned.
User avatar
darkfrei
Party member
Posts: 1181
Joined: Sat Feb 08, 2020 11:09 pm

Re: Justified text displaying one character at a time

Post by darkfrei »

Try to make it with love.graphics.print( coloredtext) https://love2d.org/wiki/love.graphics.print
table coloredtext
A table containing colors and strings to add to the object, in the form of {color1, string1, color2, string2, ...}.
table color1
A table containing red, green, blue, and optional alpha components to use as a color for the next string in the table, in the form of {red, green, blue, alpha}.
string string1
A string of text which has a color specified by the previous color.
table color2
A table containing red, green, blue, and optional alpha components to use as a color for the next string in the table, in the form of {red, green, blue, alpha}.
string string2
A string of text which has a color specified by the previous color.
tables and strings ...
Additional colors and strings.
I've tried, but without success. (Update: my failure was: I have forgot that I cannot use text as linked object as

Code: Select all

coloredText = {{1,1,1}, whiteString, {0,0,0}, blackString}
and than change whiteString / blackString without update the table :x -- )

See my solution:
Attachments
appearing-letters_01.love
(946 Bytes) Downloaded 272 times
Last edited by darkfrei on Mon Dec 11, 2023 4:26 pm, edited 5 times in total.
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
saddleblasters
Prole
Posts: 3
Joined: Tue Jan 31, 2017 11:50 pm

Re: Justified text displaying one character at a time

Post by saddleblasters »

Great idea! I modified the love.graphics.print in your example to get the exact effect I was looking for:

Code: Select all

love.graphics.printf({{1,1,1,1}, whiteString, {1,1,1,0}, blackString}, 10, 10, 200, "justify")
For anyone else looking at this, the key idea is to print all the text at once, but make the part that isn't display yet transparent. This way there's no weirdness from the justification moving stuff around.
pgimeno wrote: Mon Dec 11, 2023 12:12 pm Just use left-alignment instead of justification. Even if it worked as you expect, it would have to justify the whole line every time one is completed, and if you're still reading that, it's uncomfortable.

If you absolutely want to try it, you can try printing all lines except the last one justified, and the last one left-aligned.
I've already handled the problem with the last line. This is part of a much larger system, which is why I didn't post any code snippets. Though the solution you describe is exactly what I'd used.
User avatar
darkfrei
Party member
Posts: 1181
Joined: Sat Feb 08, 2020 11:09 pm

Re: Justified text displaying one character at a time

Post by darkfrei »

I don't like how the line "about it" looks out:
2023-12-11T17_28_07-Untitled.png
2023-12-11T17_28_07-Untitled.png (21.1 KiB) Viewed 49496 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
saddleblasters
Prole
Posts: 3
Joined: Tue Jan 31, 2017 11:50 pm

Re: Justified text displaying one character at a time

Post by saddleblasters »

darkfrei wrote: Mon Dec 11, 2023 4:28 pm I don't like how the line "about it" looks out:
I solved this by printing a paragraph at a time, left aligning the last line for each paragraph. For basic text printing it's a lot of extra work, but I was already handling paragraphs to get text to wrap around images.
Screen Shot 2023-12-12 at 10.53.17 AM.png
Screen Shot 2023-12-12 at 10.53.17 AM.png (403.59 KiB) Viewed 49421 times
The justification still isn't perfect though, which goes to show pgimeno is probably right about it not being worth the effort in most cases. How difficult was it to write your own justifier? The algorithms I see after a quick google search don't look that complicated, though that doesn't always mean the implementation is easy.

By the way darkfrei, I really like your idea of using math.random() rather than using a timer. The tiny variations in the speed of the text makes it feel more natural.
User avatar
darkfrei
Party member
Posts: 1181
Joined: Sat Feb 08, 2020 11:09 pm

Re: Justified text displaying one character at a time

Post by darkfrei »

Not justification, but you cannot see it:

Code: Select all

blackString = [[I'm trying to implement the effect you see in many games where text is printed a character at a time. (See the original Dragon Quest for an example.) On its own this is easy: all one needs to do is keep a counter then use string.sub and utf8.offset to print a substring that gets longer and longer. The trouble comes from another limitation I have: I'd like to use printf's "justify" alignment. The naive implementation of this looks weird, as though text is swooping in from the right. The reason for this is clear: the justification changes the position of text that's already printed. I'm not really sure how to solve this problem though, and I wasn't able to find anyone else posting about it.

The only workaround I can think of is to print all the text in one go, properly justified, but have it start out covered by black rectangles over each line, then shrink the rectangles a character at a time until all the text is revealed. This should work, but it seems a bit convoluted and might cause problems later down the line if, say, I want a semi-transparent textbox.]]

local font = love.graphics.getFont ()
local width = 400
local words = {}
for w in blackString:gmatch("%S+") do table.insert (words, w) print (w) end
local strings = {}

local tempSTR = ""
for i, w in ipairs (words) do
	local temp
	if tempSTR == "" then temp = w else temp = tempSTR..' '..w end
	if (i == #words) then
		table.insert (strings, {
				str = temp, 
				sx = 1
				}
			)
	elseif width < font:getWidth(temp) then
		table.insert (strings, {
				str = tempSTR, 
				sx = width/font:getWidth(tempSTR)
				}
			)
		tempSTR = w
	else
		tempSTR = temp
	end
end

function love.draw()
--	love.graphics.rectangle ('line', 10,10, width, 200) -- to be sure
	for i, s in ipairs (strings) do
		love.graphics.printf({{1,1,1}, s.str}, 10, 10+(i-1)*font:getHeight(), width, "left", 0, s.sx, 1)
	end
end
2023-12-12T07_54_30-Untitled.png
2023-12-12T07_54_30-Untitled.png (60.83 KiB) Viewed 49369 times
With

Code: Select all

love.graphics.setDefaultFilter( "nearest", "nearest")
2023-12-12T07_57_10-Untitled.png
2023-12-12T07_57_10-Untitled.png (24.47 KiB) Viewed 49370 times
:awesome:

The better result can be given if you write words with the sx = 1, but the spaces have different sx, based on amount of spaces and width of text and width of text field.
https://love2d.org/wiki/Text:addf

Code: Select all

	print (str)
	local words = {}
	for w in str:gmatch("%S+") do table.insert (words, w) end
	local spacesAmount = #words - 1
	local stringWidth = font:getWidth (str)
	local spaceWidth = font:getWidth (' ')
	local noSpacesStringWidth = stringWidth - spacesAmount * spaceWidth
	local newSpaceWidth = (width - noSpacesStringWidth)/spacesAmount
	local sxSpace = newSpaceWidth/spaceWidth
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
darkfrei
Party member
Posts: 1181
Joined: Sat Feb 08, 2020 11:09 pm

Re: Justified text displaying one character at a time

Post by darkfrei »

The fake and self made justification:

Code: Select all

blackString = [[I'm trying to implement the effect you see in many games where text is printed a character at a time. (See the original Dragon Quest for an example.) On its own this is easy: all one needs to do is keep a counter then use string.sub and utf8.offset to print a substring that gets longer and longer. The trouble comes from another limitation I have: I'd like to use printf's "justify" alignment. The naive implementation of this looks weird, as though text is swooping in from the right. The reason for this is clear: the justification changes the position of text that's already printed. I'm not really sure how to solve this problem though, and I wasn't able to find anyone else posting about it.

The only workaround I can think of is to print all the text in one go, properly justified, but have it start out covered by black rectangles over each line, then shrink the rectangles a character at a time until all the text is revealed. This should work, but it seems a bit convoluted and might cause problems later down the line if, say, I want a semi-transparent textbox.]]

love.graphics.setDefaultFilter( "nearest", "nearest")


local font = love.graphics.getFont ()
local width = 380
local words = {}
for w in blackString:gmatch("%S+") do table.insert (words, w) end

local stringLines = {}
local strings = {}

local tempSTR = ""
for i, w in ipairs (words) do
	local temp
	if tempSTR == "" then temp = w else temp = tempSTR..' '..w end
	if (i == #words) then
		table.insert (stringLines, {
				str = temp, 
				sx = 1
				}
			)
		table.insert (strings, temp)
	elseif width < font:getWidth(temp) then
		table.insert (stringLines, {
				str = tempSTR, 
				sx = width/font:getWidth(tempSTR)
				}
			)
		table.insert (strings, tempSTR)
		tempSTR = w
	else
		tempSTR = temp
	end
end

loveText = love.graphics.newText( font, "" )
for i, str in ipairs (strings) do
	print (str)
	local words = {}
	for w in str:gmatch("%S+") do table.insert (words, w) end
	local spacesAmount = #words - 1
	local stringWidth = font:getWidth (str)
	local spaceWidth = font:getWidth (' ')
	local noSpacesStringWidth = stringWidth - spacesAmount * spaceWidth
	local newSpaceWidth = (width - noSpacesStringWidth)/spacesAmount
	local sxSpace = newSpaceWidth/spaceWidth
	
	if i == #strings then
		-- last line has no justification
		newSpaceWidth = spaceWidth
	end
	
	print (spacesAmount, stringWidth, noSpacesStringWidth, spaceWidth, newSpaceWidth, sxSpace)
	
	local x = 0
	local y = (i-1)*font:getHeight()
	for j, w in ipairs (words) do
		loveText:add( w, x, y)
		x = x + font:getWidth (w)
		if (j == #words) then
			-- do nothing
		else
			x = x + newSpaceWidth
		end
	end
end

function love.draw()
	love.graphics.rectangle ('line', 10,10, width, 260)
	for i, s in ipairs (stringLines) do
		love.graphics.printf({{1,1,1}, s.str}, 10, 10+(i-1)*font:getHeight(), width, "left", 0, s.sx, 1)
	end
	love.graphics.rectangle ('line', 410,10, width, 260)
	love.graphics.draw (loveText, 410, 10)
end

function love.keypressed(key, scancode, isrepeat)
	if false then
	elseif key == "escape" then
		love.event.quit()
	end
end
2023-12-12_01.png
2023-12-12_01.png (50.36 KiB) Viewed 49350 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
RNavega
Party member
Posts: 251
Joined: Sun Aug 16, 2020 1:28 pm

Re: Justified text displaying one character at a time

Post by RNavega »

saddleblasters wrote: Mon Dec 11, 2023 11:50 am I'm trying to implement the effect you see in many games where text is printed a character at a time.

(...)

have it start out covered by black rectangles over each line, then shrink the rectangles a character at a time until all the text is revealed. This should work, but it seems a bit convoluted and might cause problems later down the line if, say, I want a semi-transparent textbox.
Just for academic interest, instead of using black rectangles, you can totally mask with transparency using a shader: you have 2 float uniforms, both in screen-space, which represent 2 "rays" that divide the screen: one being a horizontal ray that moves from top to bottom and above which all pixels are shown (for text lines already completed), and the other being a vertical ray that moves from left to right and to the left of which all pixels are shown (for the text line being revealed). Then the alpha of the pixel is this, with "texturecolor" coming from Löve's effect() pixel shader function:

Code: Select all

texturecolor.a = max(1.0-step(verticalRay, screen_coords.x), 1.0-step(horizontalRay, screen_coords.y));
(I don't remember if screen_coords uses the bottom-left GLSL origin, if so you'll have to change the Y comparison.)

All of that said, .print() and .printf() are very fast and even if the text mesh is being generated per-frame by Löve as you reveal each new character, it's going to be way easier & faster to just use these functions than making a static Text object and animating the character reveal in the shader like described above.
But anyway, it's good to know that it can be done that way -- if you wanted a soft reveal like some JRPGs have it, where the characters fade-in with transparency, then it needs to be done that way in the shader using the screen-space distance from the pixel towards the vertical ray.

I'd also like to suggest a different approach to the text speed.
The text speed as shown in darkfrei's great demo, being random, gives it a "typewriter" effect indeed, but I found it a bit distracting.
I think a constant text speed (user-adjustable in the game settings) that has small delays on each punctuation mark (also toggable in the settings) gives it a nice a "speaker" effect instead, like you can almost listen to the voice while reading.
Like using punctuation characters like these to create delays:

Code: Select all

, . ; : ! ? –
Result:
slowDialogText.gif
slowDialogText.gif (18.32 KiB) Viewed 48804 times
This is a constant 40 characters per-second reveal speed, and a 350ms delay per punctuation.
User avatar
darkfrei
Party member
Posts: 1181
Joined: Sat Feb 08, 2020 11:09 pm

Re: Justified text displaying one character at a time

Post by darkfrei »

RNavega wrote: Sat Dec 16, 2023 6:25 pm
Just for academic interest, instead of using black rectangles, you can totally mask with transparency using a shader: you have 2 float uniforms, both in screen-space, which represent 2 "rays" that divide the screen: one being a horizontal ray that moves from top to bottom and above which all pixels are shown (for text lines already completed), and the other being a vertical ray that moves from left to right and to the left of which all pixels are shown (for the text line being revealed).
Shaders are not so easy, can you please make the .love file with the simplest realization?
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
Post Reply

Who is online

Users browsing this forum: Google [Bot], Semrush [Bot] and 60 guests