Multiple colors in a line of text to highlight keywords

Started by rongel, Wed 10/04/2024 09:38:22

Previous topic - Next topic

eri0o

@Crimson Wizard , that ticket is really something that would be great. Like you would send the commands and some form of accelerated drawing would run like at the release call or in some lazyway, and then yes I agree we could have it done by script.

Quoteone could draw the full text at once, and then hide the part that should not be visible yet with an overlaying object

Yeah, if we have some way to "mask" or use a "stencil" of somehow, then I think this is the best way, because then one can do the drawing before, so the tags for color and whatever are already processed and it can have information on the size of each "token" that is getting typewritten, and in this way it works nicely even for languages that use ligatures and everything. Otherwise the typewriter module has to know about the parsing of the color/formatting tags.

But I do think we could have more nice things in Engine. As an example, there's a whole plugin (the SpriteFont) for drawing text that we maintain and features from it could be in engine itself - although it would need some thoughts on this, it's been there for a few years now.

I mean, we don't even have something to break a string in array of strings to make parsing easier. So some niceties to Strings would be nice to make it easier to parse - right now I can only think about going character by character. We also don't have regular expressions.

eri0o

Spoiler
I am trying to come up with a parser, I can't really think in a good way to structure the code to do it in one go, I am trying the following approach.

  • I am using a small stack per type of tag (for my test I am using only font and color), this is so I can go back to the previous state before the tag, but also support a little nesting
  • I parse all text into an array of text tokens that simply hold the string, color and font type, just the minimum so I can remove all tags and not lose information
  • I now read the text tokens and break in the middle of a text token (or not) per width, and also draw things

I actually don't draw directly and instead do draw commands and next do the drawing. I want to later see if I can come up with either a renderer that doesn't redraw if not needed or that is accelerated based on overlays.

It appears to work somewhat. Need to clear up a few details though before posting it here.
[close]

eri0o

Spoiler
Here is it a preliminary version that can use c as a tag for color and f as a tag for font, you can pass a value using : which is an int number that is either the AGS color value or the font index.

Code: ags
function room_AfterFadeIn()
{
  DrawingSurface* surf = Room.GetDrawingSurfaceForBackground();
  
  String ft = "[c:48214]test string[/c] hi test [f:1]font [c:64939]different[/c] working[/f] now back.";
  
  surf.DrawFancyTextWrapped(48, 48, 200, 22422, eFontNormal, ft);
}

It has a small issue with spaces which I can't quite figure it out, when the style changes it appears it doesn't properly do the space between words but not sure why yet.

Below is the module header and script


FancyText.ash
Code: ags
// fancy text module header
// by eri0o
import void DrawFancyTextWrapped(this DrawingSurface*, int x, int y, int width, int color, FontType font, const string text);

FancyText.asc
Spoiler
Code: ags
// fancy text module script

#define MAX_TXTTK 1024
#define MAX_TXTST 32

struct TextToken {
  int color;
  FontType font;
  String text;
  int width;
  int height;
};
TextToken _txttk[MAX_TXTTK];
int _txttk_count;

void _AppendTxtTok(String text, int color, FontType font)
{
  int i = _txttk_count;
  _txttk[i].color = color;
  _txttk[i].font = font;
  _txttk[i].text = text;
  _txttk[i].width = GetTextWidth(text, font);
  _txttk[i].height = GetFontHeight(font);
  _txttk_count++;
}
void _ClearTxtTok()
{
  _txttk_count = 0;
}

enum TagType {
  eTagNone = 0, 
  eTagColor, 
  eTagFont  
};

struct StackOfInt
{
  int items[MAX_TXTST];

  int index;
  import void Push(int number);
  import int Pop();
  import void Clear();
};
void StackOfInt::Push(int number)
{
  this.items[this.index] = number;
  this.index++;
}
int StackOfInt::Pop()
{
  this.index--;
  return this.items[this.index];
}
void StackOfInt::Clear()
{
  this.index = 0;
}

StackOfInt _stk_font;
StackOfInt _stk_color;

TagType _get_tag_type(String tag)
{
  int c = tag.Chars[0];
  switch(c)
  {
    case 'c': // color
      return eTagColor;
    case 'f': // font
      return eTagFont;
  }
  return eTagNone;
}

int _get_tag_data(String tag, TagType tag_type)
{
  if(tag_type == eTagNone) return 0;
  int p = tag.IndexOf(":");
  if(p <= 0) return -1;
  p++;
    
  String data = tag.Substring(p, tag.Length - p);
  
  if(data.Chars[0] >= '0' && data.Chars[0] <= '9') return data.AsInt;
  return 0;
}

void _parse_text(String text, FontType base_font, int base_color)
{
  int len = text.Length;
  bool plain_text = true;
  int color = base_color;
  FontType font = base_font;
  String ttok = "";
  
  // first element is a fake to hold length as color
  _ClearTxtTok();
  _AppendTxtTok("", len, 0);

  for (int i=0; i < len; i++) {
    int c = text.Chars[i];
  // System.Log(eLogInfo, "%c", c);
    
    if(c == '[')
    {
      if(ttok.Length > 0) {
        _AppendTxtTok(ttok, color, font);
        ttok = "";
      }
      
      i++;
      bool is_closing = false;
      if(i < len && text.Chars[i] == '/') {
        is_closing = true;
        i++;
      }
      
      int j = i;
      while (j <= len && text.Chars[j] != ']') j++;
      int delta = j - i;
      
      String strtag = text.Substring(i, delta);
      TagType tag = _get_tag_type(strtag);
      TagType tdata = tag;
      if(is_closing) tdata = eTagNone;
      int data = _get_tag_data(strtag, tdata);
      switch(tag) {
        case eTagColor:
          if(is_closing) color = _stk_color.Pop();
          else {
            _stk_color.Push(color);
            color = data;
          }
          break;
        case eTagFont:
          if(is_closing) font = _stk_font.Pop();
          else {
            _stk_font.Push(font);
            font = data;
          }
          break;
      }
      
      i += delta + is_closing;
    }
    else if(c == ' ')
    {
      ttok = ttok.AppendChar(c);
      _AppendTxtTok(ttok, color, font);
      ttok = "";
    }
    else if(c == '\n')
    {
      _AppendTxtTok(ttok, color, font);
      _AppendTxtTok("\n", color, font);
      ttok = "";
    }
    else 
    {
      ttok = ttok.AppendChar(c);
    }
  }
  _AppendTxtTok(ttok, color, font);
}

void _draw_text(DrawingSurface* surf, int x, int y, int color, FontType font, const string text)
{
  surf.DrawingColor = color;
  surf.DrawString(x, y, font, text);  
}

void _write_tokens(DrawingSurface* surf, int x, int y, int width)
{
  int start_i, end_i, p_i; 
  int len;
  int r_x = x;
  int r_y = y;
  int w;
  int itk = 0;
  int color;
  int font;
  int word_width;
  int word_height;
  int line_height;
  String word;
  
  // fake initial token encodes as color
  len = _txttk[itk].color;
  itk++;
  
  
  for(; itk < _txttk_count; itk++)
  {
    word = _txttk[itk].text;
    font = _txttk[itk].font;
    color = _txttk[itk].color;
    word_width = _txttk[itk].width;
    word_height = _txttk[itk].height;
    
    int word_len = word.Length;
    
    if(word_len <= 0) continue;
    
    if(w + word_width > width || (word_len == 1 && word == "\n"))
    {
      // line break
      r_x = x;
      r_y += line_height;
      w = 0;      
      line_height = 0;
    }
    
    if(word_height > line_height) line_height = word_height;
    w += word_width;
    
    // do draw command
    _draw_text(surf, r_x, r_y, color, font, word);
    
    r_x += word_width;
  }
}

void DrawFancyTextWrapped(this DrawingSurface*, int x, int y, int width, int color, FontType font, const string text)
{
  _parse_text(text, font, color);
  _write_tokens(this, x, y, width);
}
[close]
[close]

eri0o

Spoiler
OK, did a ton of more work and set up a repository here: github.com/ericoporto/fancy/blob/main/fancy_demo/fancy.asc

It now can do this

Code: ags
function room_AfterFadeIn()
{
  DrawingSurface* surf = Room.GetDrawingSurfaceForBackground();
  
  surf.DrawingColor = 10565;
  surf.DrawRectangle(48, 48, 248, 108);
  
  String ft = "Hello!\nCan you find me the [c:27647]blue cup [s:2041][/c]?\nI lost it in the [c:64493]dangerous [f:0]planet[/f][/c], somewhere.";
  
  surf.DrawFancyTextWrapped(48, 48, 200, 22422, eFontSpeech, ft);
}



I will try to properly release this as a module in the next days...
[close]

Matti


eri0o

Thanks @Matti !

So far I got a minimal typed text added too, in case it's needed. I am experimenting with a new tag for delaying it slightly. The text is only parsed once and the information is used later when typing, so it's not performance intensive.

I also added alignment (in the picture it's bottom left), but won't be adding it per tag, just for the whole box (would complicate the code a lot!)

I want to see if I can materialize an animated version of it - for a bouncy text or something, but I think I won't make it before first release - the animated token knows the line height, so it has an idea of the size of the area it has to move text around, but the actual animation code is a bit complicated when compared to how is everything else so far.

Other than this I haven't had many ideas, not sure what sort of utilities would be useful or not.

Edit:

Erh... Decided to just make a quick small release to see how it goes: https://www.adventuregamestudio.co.uk/forums/index.php?topic=61549.0

rongel

Sorry for the abcensce, great to see discussion about this! I'll try @eri0o your module soon, looks very nice!

Quote from: Crimson Wizard on Fri 12/04/2024 07:02:34The starting request has 2 conditions, at least from my understanding:
1. Being able to color particular words or sequence of words.
2. The text is static, so "raw" drawn once and then kept displayed without changes.

Is this correct, or was there anything else to it?

Yes, I wanted to simply highlight a specific word with a different color in a text sentence. And so that it works otherwise normally and can be translated to different language without trouble.

In addition, if that functionality could be added to dialog options (in the dialog editor), it would be even better. The dialog system is very nice and simple to use, but the lack of dialog option customization limits its creative use.

I did an ugly test to get the result I was looking for: this is made with CustomDialogGui module, and with three dialog options available. I left the top dialog option empty (actually it has one empty space) and placed a green text label in the same location. When clicking on the green label, the empty dialog option gets selected and the whole text "puzzle" works from the dialog editor. The green label is there just for the graphics.



If something like this would be possible, I think it could open up new creative ways to use the dialog system.
Dreams in the Witch House on Steam & GOG

eri0o

I don't understand, dialog options are fully cistomizeable...

https://adventuregamestudio.github.io/ags-manual/CustomDialogOptions.html

You can draw them how you want and also control them how you want. :/

rongel

Quote from: eri0o on Tue 16/04/2024 09:58:59I don't understand, dialog options are fully cistomizeable...

https://adventuregamestudio.github.io/ags-manual/CustomDialogOptions.html

You can draw them how you want and also control them how you want. :/

So it's possible to change the color of specific dialog option using custom dialog option rendering? Or to change the color of a single word? I have never touched custom dialog options rendering, I fear it's above my skills.

I'm using the CustomDialogGui module which is easy to use, but doesn't have that function. In my example, I was thinking of inserting code to the dialog option, or something similar.
Dreams in the Witch House on Steam & GOG

eri0o

I tried to do a write up on custom dialogues once, I think I managed to find it here

https://www.adventuregamestudio.co.uk/forums/beginners-technical-questions/about-dialog-options-and-templates/msg636637779/#msg636637779

I think chomba asked more questions on custom dialogues that had more answers at the time but I can't find them now.

I found a few more maybe somewhat relevant or not links here

https://www.adventuregamestudio.co.uk/forums/beginners-technical-questions/highlight-buttons-text-and-displayhide-arrows-in-dialog-gui/

https://www.adventuregamestudio.co.uk/forums/beginners-technical-questions/custom-dialogue-issue-with-dimensions/msg636621681/#msg636621681

https://www.adventuregamestudio.co.uk/forums/beginners-technical-questions/personalizing-dialogs-and-giving-them-a-custom-design/msg636654971/#msg636654971

@rongel

I would suggest first if you can find some screenshot that is exactly what you want or if you can draw in a paint program, to do a mockup of exactly what you want, then it should be easier.

If you play my I Rented a Boat game it has two simple custom dialog with slightly differences (one is meant to be selected and force you to select before advancing the game and the other is meant to let the player still walk around when presented, like in Firewatch), the code for those is here: https://github.com/ericoporto/i_rented_a_boat/blob/main/I_rented_a_boat/CustomSay.asc

rongel

Quote from: eri0o on Tue 16/04/2024 13:21:57I would suggest first if you can find some screenshot that is exactly what you want or if you can draw in a paint program, to do a mockup of exactly what you want, then it should be easier.
The thing is that I'm still figuring it out myself, and still testing different mechanics. BUT I do have a working prototype, made with ugly tricks and deceit.

Basically it's a short text-based multiple choice event, made with a custom dialog system. Clicking on the dialog option updates the description label and gives new dialog options. In the end, some results may end up positive or negative. Different coloring will highlight the special dialog options and positive / negative results. A rough example:

       


       

So currently I have two requests:

  • Edit the color of a specific word in dialog options (image 1)
  • Edit the color of a specific word in a text label (image 4)

I think I could do this with cheating (placing color labels under dialog options, like in image 1), or by having multiple labels (like in image 4), but it would be nicer if there was an easier way.

I hope this makes sense!
Dreams in the Witch House on Steam & GOG

Khris

My guess is the custom dialog module already uses dialog_options_render() under the hood, so you'd have to dive into that to have colored options.

As for label text, AGS doesn't support more than one color per label. You'd have to use a button instead and draw its NormalGraphic.

In both cases you can use eri0o's module to have multiple colors in your text.

eri0o

Hey, I kinda don't remember, when showing dialog options in AGS, does it already has something to make the "title text"? Like in that screen you show options at the bottom and a text at the top, how do you set that text?

I am trying to think how to enable something like that API wise.

Crimson Wizard

#33
Quote from: eri0o on Wed 17/04/2024 16:15:08Hey, I kinda don't remember, when showing dialog options in AGS, does it already has something to make the "title text"? Like in that screen you show options at the bottom and a text at the top, how do you set that text?

I am trying to think how to enable something like that API wise.

In custom dialog options you draw everything yourself using DrawingSurface.

eri0o

Right, I was trying to think about the "Description" text in Rongel's case. I guess I have to create a new function with something like

Code: ags
void FancyStart(this Dialog*, const string description) ...

And then later once the dialog is over I clean it up - I know there's an event to check when a dialog has ended but I think I need to handle in rep exec always anyway to be able to detect end of dialog even if something that is blocking is running.

rongel

Quote from: eri0o on Wed 17/04/2024 16:15:08Hey, I kinda don't remember, when showing dialog options in AGS, does it already has something to make the "title text"? Like in that screen you show options at the bottom and a text at the top, how do you set that text?

That is made with a GUI with a label on top. The custom dialog gui is placed at the bottom. When player selects a dialog option from the bottom, it updates the text label at the top. So there is no actual speech or dialog, just a label.Text command inside the dialog editor.
Dreams in the Witch House on Steam & GOG

eri0o

I added a new functionality to my module that lets you "Fancify" a button. I explained in my last post. But essentially you can do something like

Code: ags
Button1.Fancify(Fancy9Piece.CreateFromTextWindowGui(gTextBorder));

And it will adjust the button by feeding the text in the module to parse, generate a sprite and can optionally puts this text inside a 9-piece box - you can also pass three of these 9-piece if the button is meant to be interacted (the normal, mouse over and pushed graphics).

If no such textbox around the text is desired you can pass null too.

@rongel maybe this is helpful, let me know. The custom dialog though I think it's better if you read on and make your own version of it.

rongel

Quote from: eri0o on Thu 18/04/2024 01:36:19@rongel maybe this is helpful, let me know. The custom dialog though I think it's better if you read on and make your own version of it.
Thanks, I really appreciate your work on this, hopefully your module will help others as well. I'm currently working on something else, but I'll delve into your module more closely next week.
Dreams in the Witch House on Steam & GOG

Crimson Wizard

Quote from: eri0o on Thu 18/04/2024 01:36:19@rongel maybe this is helpful, let me know. The custom dialog though I think it's better if you read on and make your own version of it.

I also think it's a right thing if users implement options rendering on their own, using only basic elements from your module (single text drawing, etc). Because each game needs its own look, the arrangement of elements, extra elements (not related to option text) etc, - that's unlimited combinations impossible to predict.

Another thing, potentially, if there are 2 implementations of "fancy text", one of which draws upon a raw drawing surface, and another generates everything using overlays, I suppose (cautiously) that this may still work even with the dialog options, because they should not prevent extra elements created or turned on/off during their display. But there will be a problem of z-sorting, or rather receiving information of what z coordinate does the "main surface" have; this has to be passed somehow...

SMF spam blocked by CleanTalk