Displaying an image on pause

Started by Max, February 27, 2018, 04:32:05 PM

Previous topic - Next topic
Awesome! So much learning. Thanks again.

Since you're still learning, I'll post some examples of other things you can do too.

One trick you can do is change the color of a bitmap font. In your case it works great because your font is black and white.

To change all black pixels to another color while leaving white pixels unchanged is to use the add blend mode. Think of black as having a value of 0 and white having a value of 1. black + color2 = color2 (0 plus anything is the value you added); white + color2 = white (because 1 for white is already the max value).

And likewise to change all white pixels to another color while leaving black pixels unchanged is to use the multiply blend mode. white * color2 = color2 (1 times anything is the value you multiplied by); black * color2 = black (0 times anything is 0).

This code shows an example of changing black to light blue (see top left screenshot) at lines 48-51. Lines 54-57 change white to yellow (see top right). You don't want to do both at the same time because that will cause funny results (since then there'd be more colors than just black and white present).

Code (lua) Select
pause_infra = {}
local current_log = sol.surface.create(138,72) --22 characters across, 4+ lines tall

local text_surface = sol.text_surface.create({
    vertical_alignment = "top",
    horizontal_alignment = "center",
    font = "oceansfont",
})
--note that a size is not specified for a text surface, it will be as big as the text it contains

--Returns iterator to split text at line breaks
local function line_it(text)
    assert(type(text)=="string", "Bad argument #1 to 'line_it' (string expected, got "..type(text)..")")

    text = text:gsub("\r\n", "\n"):gsub("\r", "\n").."\n" --convert instances of \r to \n and add line break to end
    return text:gmatch("([^\n]*)\n") -- Each line including empty ones.
end


function pause_infra:update_game(game)
    current_log:clear() --erase any previous content on surface

    local log_a_text = game:get_value("quest_log_a") --the string saved to the game value "quest_log_a" should match a key in strings.dat
    local next_line = line_it(sol.language.get_string(log_a_text)) --gets localized string in current language

    text_surface:set_text(next_line()) --line 1 of quest_log_a
    text_surface:draw(current_log, 69, 0) --renders first line of quest_log_a text and draws on current_log surface
        --x value 69 because horizontal alignment is center (138/2 = 69)

    text_surface:set_text(next_line()) --line 2 of quest_log_a
    text_surface:draw(current_log, 69, 16)

    --NOTE: if quest_log_a contains more than 2 lines of text then the extra lines do not get drawn

    local log_b_text = game:get_value("quest_log_b")
    next_line = line_it(sol.language.get_string(log_b_text))

    text_surface:set_text(next_line()) --line 1 of quest_log_b
    text_surface:draw(current_log, 69, 39)

    text_surface:set_text(next_line()) --line 2 of quest_log_b
    text_surface:draw(current_log, 69, 55)

    --Now current_log contains and image rendered from text strings of log A & log B (up to 4 lines)
    --content won't change while pause menu open
   
    --use this trick to change the color of black pixels to something else
    local blend_add_surface = sol.surface.create(current_log:get_size()) --same size as current_log
    blend_add_surface:fill_color{0,128,255} --light blue
    blend_add_surface:set_blend_mode("add") --allows changing black pixels without changing white pixels
    blend_add_surface:draw(current_log)
   
    --use this trick to change the color of white pixels to something else
    --[[local blend_mult_surface = sol.surface.create(current_log:get_size()) --same size as current_log
    blend_mult_surface:fill_color{255,255,0} --yellow
    blend_mult_surface:set_blend_mode("multiply") --allows changing white pixels without changing black pixels
    blend_mult_surface:draw(current_log)]]
end


local pause_img = sol.surface.create("hud/pause_infra.png")

function pause_infra:on_draw(dst_surface)
    --draw menu architecture
    pause_img:draw(dst_surface)

    --draw quest log
    current_log:draw(dst_surface, 167, 27) --specify coordinates for where you want to draw it on the screen
end


Another application for this technique is you can use it to change the color of just some words. You would do this by specifying x, y, width and height values when you call the fill_color() function of the blend mode surface. That way only the text within that rectangle would change color. You'd have to figure out the position of the words that you want to have a different color.




If you wanted to use a non-bitmap font, here's how you could do it. The trick here is drawing the text twice. The first time draws a shadow in one color, then the second pass draws the text again in a different color with a slight offset. This makes the text easier to read against a wide variety of background colors when using a semi-transparent background.

The font I used is EncodeSansCompressed-SemiBold, which you can find here (Open SIL license).

(see bottom left screenshot)
Code (lua) Select
pause_infra = {}
local current_log = sol.surface.create(138,72)

local FONT_COLOR = {255, 255 ,255} --white
local FONT_SHAODW_COLOR = {255,0,0} --red

local text_surface = sol.text_surface.create({
    vertical_alignment = "top",
    horizontal_alignment = "center",
    font = "EncodeSansCompressed-SemiBold",
    font_size = 14,
    rendering_mode = "antialiasing",
})
--note that a size is not specified for a text surface, it will be as big as the text it contains

--Returns iterator to split text at line breaks
local function line_it(text)
    assert(type(text)=="string", "Bad argument #1 to 'line_it' (string expected, got "..type(text)..")")

    text = text:gsub("\r\n", "\n"):gsub("\r", "\n").."\n" --convert instances of \r to \n and add line break to end
    return text:gmatch("([^\n]*)\n") -- Each line including empty ones.
end


--This function consolidates repeated lines of code to reduce redundancy in pause_infra:update_game()
local function draw_text(text, dst_surface, x, y)
    --ensure inputs valid
assert(type(text)=="string", "Bad argument #1 to 'draw_text' (string expected, got "..type(text)..")")
assert(sol.main.get_type(dst_surface)=="surface", "Bad argument #2 to 'draw_text' (surface expected, got "..sol.main.get_type(dst_surface)..")")
x = math.floor(tonumber(x) or 0) --try to force x to integer or use 0 instead
y = math.floor(tonumber(y) or 0)

text_surface:set_text(text)

    text_surface:set_color(FONT_SHAODW_COLOR)
    text_surface:draw(current_log, x+1, y+1) --draw shadow text first, offset 1 pixel to the right and down
   
    text_surface:set_color(FONT_COLOR)
    text_surface:draw(current_log, x, y) --draw non-shadow text on top of shadow text
end


function pause_infra:update_game(game)
    current_log:clear() --erase any previous content on surface

    local log_a_text = game:get_value("quest_log_a") --the string saved to the game value "quest_log_a" should match a key in strings.dat
    local next_line = line_it(sol.language.get_string(log_a_text)) --gets localized string in current language

    draw_text(next_line(), current_log, 69, 0) --renders first line of quest_log_a text and draws on current_log surface
         --x value 69 because horizontal alignment is center (138/2 = 69)
    draw_text(next_line(), current_log, 69, 16) --line 2 of quest_log_a

    --NOTE: if quest_log_a contains more than 2 lines of text then the extra lines do not get drawn

    local log_b_text = game:get_value("quest_log_b")
    next_line = line_it(sol.language.get_string(log_b_text))

    draw_text(next_line(), current_log, 69, 39) --line 1 of quest_log_b
    draw_text(next_line(), current_log, 69, 55) --line 2 of quest_log_b

    --Now current_log contains and image rendered from text strings of log A & log B (up to 4 lines)
    --content won't change while pause menu open
end


local pause_img = sol.surface.create("hud/pause_infra.png")

function pause_infra:on_draw(dst_surface)
    --draw menu architecture
    pause_img:draw(dst_surface)

    --draw quest log
    current_log:draw(dst_surface, 167, 27) --specify coordinates for where you want to draw it on the screen
end


Note that the new draw_text function is just to consolidate repeated code and make things cleaner.

Be aware that there is nothing to prevent the text from clipping in this example, and since I am using a font that isn't monospaced, the number of characters that can fit on a line will vary.

If you had actually wanted to use a non-bitmap, non-monospaced font, then it would probably be best to determine where the line breaks should occur dynamically in the code rather than manually entering the \n line breaks. This is getting into more details than I want to get into, but the gist of it is to split the text at the spaces and add one word at a time, checking the current text width using text_surface:get_size() and moving the new words to a new line once the length is exceeded.




Finally, one more thing you can do is create the borders you have around your quest log text box dynamically so that you don't need to use a different image for each size box you use. You would create a new image that contains the four corners of your box (7x7 pixels each) plus the repeating horizontal and vertical segments (1x7 and 7x1 pixels), plus 1 pixel in the center, giving a total size of 15x15 pixels.

To simplify things, steal my ui_draw.lua file from one of my sample quests here and put it in your scripts folder. You'll also need the pause_infra_small.png image I've attached which is derived from your original image. Then the following code will work.

One thing to note is I deleted the transparent pixels in the middle of the pause_infra.png image and am generating the transparent background dynamically in the code. This allows you to change the background color however you see fit. In the code I changed the background of the bottom box to be blue so you can see this in effect (see bottom right screenshot).

The following code generates an image similar to your pause_infra.png from just my pause_infra_small.png. This includes having to draw the "QUEST LOG" text separately as well as the line below it.

Code (lua) Select
pause_infra = {}
local current_log = sol.surface.create(138,72) --22 characters across, 4+ lines tall

local text_surface = sol.text_surface.create({
    vertical_alignment = "top",
    horizontal_alignment = "center",
    font = "oceansfont",
})
--note that a size is not specified for a text surface, it will be as big as the text it contains

--Returns iterator to split text at line breaks
local function line_it(text)
    assert(type(text)=="string", "Bad argument #1 to 'line_it' (string expected, got "..type(text)..")")

    text = text:gsub("\r\n", "\n"):gsub("\r", "\n").."\n" --convert instances of \r to \n and add line break to end
    return text:gmatch("([^\n]*)\n") -- Each line including empty ones.
end


function pause_infra:update_game(game)
    current_log:clear() --erase any previous content on surface

    local log_a_text = game:get_value("quest_log_a") --the string saved to the game value "quest_log_a" should match a key in strings.dat
    local next_line = line_it(sol.language.get_string(log_a_text)) --gets localized string in current language

    text_surface:set_text(next_line()) --line 1 of quest_log_a
    text_surface:draw(current_log, 69, 0) --renders first line of quest_log_a text and draws on current_log surface
        --x value 69 because horizontal alignment is center (138/2 = 69)

    text_surface:set_text(next_line()) --line 2 of quest_log_a
    text_surface:draw(current_log, 69, 16)

    --NOTE: if quest_log_a contains more than 2 lines of text then the extra lines do not get drawn

    local log_b_text = game:get_value("quest_log_b")
    next_line = line_it(sol.language.get_string(log_b_text))

    text_surface:set_text(next_line()) --line 1 of quest_log_b
    text_surface:draw(current_log, 69, 39)

    text_surface:set_text(next_line()) --line 2 of quest_log_b
    text_surface:draw(current_log, 69, 55)

    --Now current_log contains and image rendered from text strings of log A & log B (up to 4 lines)
    --content won't change while pause menu open
end

--create image for quest log
local ui_draw = require("scripts/ui_draw.lua")
local pause_borders = sol.surface.create("hud/pause_infra_small.png") --15x15 image
local pause_img = sol.surface.create(152,103) --size to make the quest log box
pause_img:fill_color( --first draw semi-transparent background
    {0,0,0,204}, --black with 80% opacity
    1,1,152-2,103-2 --fill surface with color except for 1 pixel border around edge
)
ui_draw.draw_frame(
{ --table describing the source image to use
surface = pause_borders,
border = 7, --x & y offset to use for determining repeating section of border image
},
pause_img --border will be drawn on top of existing pause_img surface
)
text_surface:set_text("QUEST_LOG") --this string should come from a new entry in string.dat to facilitate localization to other languages
--text_surface:set_text(sol.language.get_string("interface.title.quest_log")) --do something like this instead of line above once you create the new string.dat entry
text_surface:draw(pause_img, 69, 3) --draw QUEST_LOG title text, you may have to temporarily change the horizontal alignment to center first if using left for objectives text
pause_img:fill_color({0,0,0},8,19,136,3) --draw line under title, starting with black outline
pause_img:fill_color({255,255,255},8+1,19+1,136-2,3-2) --draw white line under title

--create image below quest log
local pause_img2 = sol.surface.create(152,46) --size to make bottom box
pause_img2:fill_color(
{0,64,128,204}, --let's make this one blue because why not?
1,1,152-2,46-2
)
ui_draw.draw_frame({surface=pause_borders, border=7}, pause_img2)

function pause_infra:on_draw(dst_surface)
    --draw menu architecture
    pause_img:draw(dst_surface, 160, 1)

    --draw quest log
    current_log:draw(dst_surface, 167, 27) --specify coordinates for where you want to draw it on the screen
   
    --draw bottom box
    pause_img2:draw(dst_surface, 160, 105)
end

Thanks, llamazing! Those are neat tricks, basically since multiply is darkening pixels and you can't darken black, you can use it to affect white, and vice-versa? Awesome. I might probably take that tip and try to work it into the ALTTP dialog box script, so I can put something like $b in my dialogs to turn the text blue to indicate the hero is talking, then $w to turn it back to white for NPCs.

Not that I should get started on that until I finish up my current to-do list of things that need wrapped up before a like, demo 1 kind of thing, but it's nice to know some of what the to-do list holds for the future!

There is also an old script I made (a modified version of Chris' dialog box script) that allows to put particular words in a different color (it only works with monospaced fonts), and also allows to put sprite images (with animations) in the dialog box (useful to talk with NPCs).
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Oh, that's super cool! Is that something you have on the site somewhere for others to use, or would you rather hold onto it yourself. I totally understand if it's the second, and I'll eventually figure it out myself.

Nope, it is meant to be used by anyone. There is an old thread about it:
http://forum.solarus-games.org/index.php/topic,312.msg1179.html#msg1179
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."