DrawString anti-aliased and with outline

Started by Snarky, Sun 03/12/2017 13:33:54

Previous topic - Next topic

Snarky

This code is part of the SpeechBubble module I just posted, but since it has more general-purpose application, I thought it deserved its own post.

Displaying text with a TrueType (.ttf) font in AGS has some well-known limitations:

  • If you place anti-aliased text on a semi-transparent background (e.g. as a label on a semi-transparent GUI), the anti-aliasing "pokes holes" in the background. This looks terrible.
  • With anti-aliased text, the AGS auto-outline feature often does not work very well, with gaps and other artifacts.
  • The auto-outline can only be one pixel wide.
  • The outline can only be black. (You can set it to a different color using game.text_shadow_color, but it will affect ALL the text on the screen.)

Here are two functions to work around those issues. They take advantage of the fact that DrawingSurface.DrawImage() does correct alpha-blending. You can use this to draw an anti-aliased string onto any DrawingSurface without artifacts. And by "stamping" multiple copies of the string in a pattern of 1-pixel displacements, you can also create an arbitrarily wide outline of any color you like.

The drawback is that it is slower, particularly if you want to draw a very wide outline (a 5-pixel outline will do something like 20 DrawImage() operations). However, I've found that in practice it runs fast enough to not be a problem for typical uses. (There are also some optimizations still possible in this code, particularly if you don't need the ability to draw the text at <100% opacity.)

Code: ags
/// The shape of a text outline
enum TextOutlineStyle
{
  /// A circular, rounded outline
  eTextOutlineRounded = 0, 
  /// A square "block" outline
  eTextOutlineSquare
};

// To work around the AGS bug where antialiasing "pokes holes" in semi-transparent canvases
void drawStringWrappedAA(this DrawingSurface*, int x, int y, int width, FontType font, Alignment alignment, String message, int transparency)
{
  DynamicSprite* textSprite = DynamicSprite.Create(this.Width, this.Height, true);
  DrawingSurface* textSurface = textSprite.GetDrawingSurface();
  textSurface.DrawingColor = this.DrawingColor;
  textSurface.DrawStringWrapped(x, y, width, font, alignment, message);
  textSurface.Release();
  this.DrawImage(0, 0, textSprite.Graphic, transparency);
  textSprite.Delete();
}

// Draw a string with outline (make sure the canvas has at least outlineWidth pixels on each side of the string)
void drawStringWrappedOutline(this DrawingSurface*, int x, int y, int width, TextOutlineStyle outlineStyle, FontType font,  Alignment alignment, String message, int transparency, int outlineColor, int outlineWidth)
{
  // This is what we draw on (because we might need to copy with transparency)
  DynamicSprite* outlineSprite = DynamicSprite.Create(this.Width, this.Height, true);
  DrawingSurface* outlineSurface = outlineSprite.GetDrawingSurface();
  
  // This holds multiple horizontal copies of the text
  // We copy it multiple times (shifted vertically) onto the outlineSprite to create the outline
  DynamicSprite* outlineStripSprite = DynamicSprite.Create(this.Width, this.Height, true);
  DrawingSurface* outlineStripSurface = outlineStripSprite.GetDrawingSurface();
  
  // This is our "text stamp" that we use to draw the outline, we copy it onto outlineStripSprite
  DynamicSprite* textSprite = DynamicSprite.Create(this.Width, this.Height, true);
  DrawingSurface* textSurface = textSprite.GetDrawingSurface();
  
  // Draw our text stamp
  textSurface.DrawingColor = outlineColor;
  textSurface.DrawStringWrapped(x, y, width, font, alignment, message);
  textSurface.Release();
  
  switch(outlineStyle)
  {
    case eTextOutlineRounded:
    {
      // Draw Circular outline
      int maxSquare = outlineWidth*outlineWidth+1; // Add 1 for rounding purposes, to avoid "pointy corners" 
      int maxWidth = 0;
      outlineStripSurface.DrawImage(0, 0, textSprite.Graphic);
      // We loop from top and bottom to the middle, making the outline wider and wider, to form circular outline
      for(int i = outlineWidth; i > 0; i--)
      {
        // Here's the circular calculation...
        while(i*i + maxWidth*maxWidth <= maxSquare)
        {
          // Increase width of the outline if necessary
          maxWidth++;
          outlineStripSurface.DrawImage(-maxWidth, 0, textSprite.Graphic);
          outlineStripSurface.DrawImage(maxWidth, 0, textSprite.Graphic);
          outlineStripSurface.Release();
          outlineStripSurface = outlineStripSprite.GetDrawingSurface();
        }
        // Draw outline strip above and below
        outlineSurface.DrawImage(0, -i, outlineStripSprite.Graphic);
        outlineSurface.DrawImage(0, i, outlineStripSprite.Graphic);
      }
      // Finally the middle strip
      outlineSurface.DrawImage(0, 0, outlineStripSprite.Graphic);
      break;
    }
    case eTextOutlineSquare:
    {
      // Draw square block outline
      // Just draw the full outline width onto the strip
      for(int i = -outlineWidth; i <= outlineWidth; i++)
        outlineStripSurface.DrawImage(i, 0, textSprite.Graphic);
      outlineStripSurface.Release();
      // Draw the full outline height
      for(int j = -outlineWidth; j <= outlineWidth; j++)
        outlineSurface.DrawImage(0, j, outlineStripSprite.Graphic);
      break;
    }
  }
  textSprite.Delete();
  outlineStripSurface.Release();
  outlineStripSprite.Delete();
  
  /// Now draw the text itself on top of the outline
  outlineSurface.DrawingColor = this.DrawingColor;
  outlineSurface.drawStringWrappedAA(x, y, width, font, alignment, message, 0);
  outlineSurface.Release();
  // ... And copy it onto our canvas
  this.DrawImage(0, 0, outlineSprite.Graphic, transparency);
  outlineSprite.Delete();
}

Monsieur OUXX

Would you be kind enough to add a small paragraph with a link to this on the wiki, on the Fonts page?
 

SMF spam blocked by CleanTalk