DmiFontsPlus

A utility/library combo for BYOND by Lummox JR

Version 1.0

DmiFontsPlus is a revamp of DmiFonts that uses functionality new to BYOND 4.0. Because the icon format and procs have changed, this is packaged as a new library. Most of the procs are similar to before, but the main differences are that the /iconset datum no longer exists, and procs that used to use it now work with /icon directly. As with all big icons, state "0,0" is the lower left corner.

The new icon format used by DmiFontsPlus has white text on a transparent background, and uses alpha transparency. It is important when switching to this library to re-make your font.dm and font.dmi files with the DmiFontsPlus utility.

The DmiFontsPlus Utility

This library comes with a Windows utility, written in Visual C++. The purpose of the program is to create a font.dm code file and a font.dmi icon file to go with it, which can be compiled into your programs and used easily. You will need a pair of files for every font and style you want to use. Pick a font face, point size, bold or italic, and decide what level of anti-aliasing you want to use.

Figure 1: The DmiFontsPlus program. Creating a font for use in DM is as easy as selecting a font and size and clicking Save.

Press the Font button to select a font, which will then be displayed in the window below. You can choose what to display as sample text, to decide better which font you'd prefer to use before creating any files. After choosing an anti-alasing level, press Save to create the files for your font. Then move the files to your project, and you can start using them.

The program is capable of saving different font scripts, so a font that supports Greek characters can be saved in a Greek version. (DM doesn't support Unicode, and generic Win32 doesn't support it well.)

You're likely to find for italic fonts that the overhang var has not been set; it should be, for italics. When Windows italicizes many fonts the overhang isn't set correctly. If there is no overhang, the utility will make a guess at a good value but will not fill it in for you.

Anti-Aliasing

Anti-aliasing determines how "smooth" your font appears. There can be advantages as well as drawbacks to using this technique. To anti-alias a font, the utility blows it up a certain number of times, then scales it down using shades of gray. A non-antialiased font will just be black-and-white, and is good for places where you want just text with a transparent background. The more anti-aliasing you use, the more shades of gray you get. A heavily anti-aliased font will try to show more detail, but at the same time it might make your text too fuzzy to read if you use a small point size.

The number in the box you select determines how many times to scale up the font for anti-aliasing. If you select 4, then the font is blown up to 4× its width and height, then scaled back down into 17 shades of gray (including black and white). The number of gray shades you get is n2+1, where n is the scaling factor. You can only go as high as 256 shades at most, if you pick n=16.

The DmiFontsPlus Library

The library consists of a datum which is used to keep info about the font, and new procs available to the /icon datum. The /dmifont datum defines the font's properties, including the size of the characters and their widths. It can be used both for drawing text and measuring.

Drawing Text

It's important to understand the coordinate system used by this library. If you want to draw text at the very upper left, for example, you'd draw it at position (0,0). If you want to draw text exactly centered, you have to use (16-width/2,16-height/2). You can find the width of your text using the dmifont.GetWidth(text) proc, and the height from dmifont.CountLines(text) * dmifont.height. There's a little more to it than that, but that will be covered later.

Figure 2: The layout of a 96×64-pixel icon. On the left are the coordinates used by DmiFontsPlus for drawing. On the right are the icon states you would use when assigning this icon to six atoms.

Once text has been drawn, the icon states can be assigned to atoms. You can tell how many icons are in the set by checking the setwidth and setheight vars that have been added to the /icon datum. These are not the actual full width and height, but the number of tiles wide and tall. E.g., a 96×32-pixel icon has a setwidth of 3 and a setheight of 1. Its icon states are "0,0", "1,0", and "2,0". Here's a quick example that uses an icon only one tile high, but arbitrarily wide.

obj/killcounter
  screen_loc = "SOUTHEAST"

  New(client/C)
    C.screen += src
    Update(C.mob.kills)

  proc/Update(kills)
    var/icon/I = font.DrawText(...)   // we'll get back to this
    overlays = list()
    var/obj/O = new/obj
    O.layer = 10
    O.icon = I
    for(var/i = 0, i < I.setwidth, ++i)
      O.pixel_x = (i - I.setwidth + 1) * 32
      O.icon_state = "[i],0"
      overlays += O

The call to DrawText() will be explained in the next section. For now, what you need to know is this: When the /icon is created, by default it uses blank transparent icons. The text drawn by DrawText() is white on a transparent background.

Because BYOND's icon_state coordinates put 0,0 at the lower left, here's how that loop would look if the /iconset could be more than one tile high:

    for(var/j = 0, j < I.setheight, ++j)
      O.pixel_y = j * 32
      for(var/i = 0, i < I.setwidth, ++i)
        var/icon/ic = s.GetIcon(i, 0)
        if(ic)
          O.pixel_x = (i - I.setwidth + 1) * 32
          O.icon_state = "[i],[j]"
          overlays += O

Colored Text And Outlines

Since text created by DmiFontsPlus defaults to white on a black background, you'll often want to change it. To change the foreground color, you can simply add or multiply the color you want via Blend(). To change the background, you can use Blend() with a solid color and ICON_UNDERLAY.

// make text red-on-black
icon.Blend(rgb(255, 0, 0), ICON_MULTIPLY)
icon.Blend(rgb(0, 0, 0), ICON_UNDERLAY)

More often, you'll want to add an outline to your text. Using the icon.DFP_Outline() proc, you can change the foreground color and add an outline at the same time. There are two arguments: The foreground color, and the outline color:

// red text with a white outline
icon.DFP_Outline(rgb(255, 0, 0), rgb(255, 255, 255))

You can also expand the text outward using icon.DFP_Dilate(), which takes no arguments.

Using font files

Once you've run the program and created the .dm and .dmi files for a font, and copied those fonts over to your project directory, you can use them. First, click the checkbox on yourfont.dm to make sure it compiles with your project. Now open it up. You'll see something a lot like this.

dmifont/ArialBold7pt_AA16
    name = "Arial Bold 7pt (AA 16)"
    height = 11
    ascent = 9
    descent = 2
    avgwidth = 5
    maxwidth = 24
    overhang = 0
    inleading = 2
    exleading = 0
    defchar = 31
    start = 31
    end = 255

    antialias = 16

    metrics = list(\
        1, 4, 1,	/* char 31 */ \
        0, 0, 2,	/* char 32 */ \
        ...
        0, 5, 0,	/* char 255 */ \
        225)

    defined = list(\
        null, null, ... null,\
        ...
        ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<",\
        ... )

    icon = 'ArialBold7pt_AA16.dmi'

The program has gone ahead and filled in all the values needed to use this font. So the first step to use the font in your own game is to initialize it.

var/dmifont/ArialBold7pt_AA16/tinyfont = new

Besides all the vars you can use like height, there are several important procs. GetWidth() will give you the width of a line of text, or the longest width of more than one line. CountLines() will tell you how many lines you have, where each line is separated by a \n newline character.

That's almost enough for very crude text output, so let's go back to that kill counter example. I'd like to make the text right-aligned to the screen, and drawn at the very bottom.

obj/killcounter
  proc/Update(kills)
    var/txt = "[kills] kill\s"
    var/size = font.RoundUp32(font.GetWidth(txt))
    var/icon/I = font.DrawText(txt, size, 32 - font.height,\
                               flags = DF_JUSTIFY_RIGHT, icons_y = 1)

First, it's important to know just how big an /icon will be needed to draw this text, so RoundUp32() will take the value of font.GetWidth(txt) and round it up to a multiple of 32 pixels: the size of an icon. So if the text is, say, 81 pixels wide, the next highest multiple of 32 is 96, which is 96÷32 = 3 icons wide.

Now in DrawText(), the size, 32 - font.height portion looks simple enough: Those are the coordinates where the text should be drawn. Since it's right-aligned, text will be drawn out to the left of those coordinates. But it's still drawn downward. 32 is the y coordinate just past the bottom edge of the icon, so going up by font.height, subtracting it from 32, will draw text as low down as it can go. (Actually you can draw even lower. If you don't use any descending characters like a lowercase y, just subtract font.ascent instead.)

The two arguments that may not look as clear are flags and icons_y. In flags you can specify options for word wrapping and justification; this text is right-justified, so it uses the flag DF_JUSTIFY_RIGHT. The icons_y var is a limit for how many tiles to use when creating the icon; if you don't give it a limit it will expand as far as the text. Since the example only calls for one tile's worth of height, icons_y is set to 1. There's also an icons_x if you want to limit the width, too--but text will try to draw itself right on past that.

Limiting Text

You can constrain text even further using the width argument, and maxlines. Text will then wrap words to try to fit within the limits you demand. Depending on the flags you use, it may just cut off when it runs out of room, or it may trail off in an ellipsis (...) instead.

var/icon/I = font.DrawText(mylifestory, 0, 0, width = 160, maxlines = 10,\
                           flags = DF_WRAP_ELLIPSIS)

Some life stories are shorter than others, but you'd probably see your text cut off on the 10th line with an ellipsis after it. If you wanted to show the rest of it later, and need to know where you left off, send a list (it must already be initialized) to the proc as leftover.

var/list/nexttext = new
var/icon/I = font.DrawText(mylifestory, 0, 0, width = 160, maxlines = 10,\
                           flags = DF_WRAP_ELLIPSIS, leftover = nexttext)

The list will come back either empty, or with a string starting on the 11th line of mylifestory.

You can also indent your text using firstline. If you set that to 10, DrawText() will indent the first line by 10 pixels. Or you can use it for hanging indents, by making it a negative value. (Note: If you use a negative firstline, the first line is allowed to be even wider than width by that amount. If firstline=-20, the first line may be 20 pixels wider than the others.)

Preformatting with GetLines()

Often it's helpful to preformat text before sending it to DrawText(). That way you can know just how wide it will be when formatted, or how many lines it will have. To do that, use the GetLines() proc. It's practically the same as DrawText(), but it leaves out anything related to the drawing itself like the x,y coordinates, icons_x and icons_y, etc. It returns a string, broken up into lines with \n where DrawText() would have broken it up. Using GetWidth() and CountLines() on the result can help you fine-tune where you want to put everything before you draw it. If you use GetLines() to preformat your text, you can also use the DF_NO_FORMAT flag in DrawText() to speed up drawing.

Word breaks

Word wrapping is done at the best possible places: at a space if one is handy, or at a forced line break (\n). If no break is available, a word will be split up just before reaching the maximum width and continued on the next line. However you may want to provide break points of your own, such as after a hyphen. Any character with an ASCII value under 10 is considered a "soft break". The character won't display normally, but will allow text to be broken up at that point.

The tab character \t is a soft break character; in ASCII it's 9. A good place to use it would be at the end of punctuation, if for some reason no space was put there, or after a hyphen. Another soft break is ASCII 8, the "hyphen break". (In the C language, ASCII 8 is \b for backspace, but in DM it has no equivalent and there's no easy way to add it to a string except by using ascii2text(8). Sorry.) The hyphen break will insert a hyphen if it's used as a soft break (and will only allow the word to be broken there if a hyphen fits).

Spacers are also available for text justification. ASCII characters 1 through 7 are justification characters, representing 1 through 7 pixels of extra padding. Do not rely on these remaining constant, however, as new soft breaks may be added in the future if necessary.

More Examples

The uses of this library are limitless. By exploring your options you'll probably discover some unique ideas that no one has even imagined yet. You can make an interface really sparkle, or personalize a game, or make it easier to tell players apart when custom colors or icons just aren't enough.

Name Labels

One idea that appears in the demo is to draw a name beneath each player when the log in. This can make it a lot easier to tell who's who in the thick of a game.

var/dmifont/Arial7pt/namefont = new

mob
  Login()
    // find the most lines we can fit in 1 icon's height
    var/lines = round(32 / namefont.height)
    var/txt = namefont.GetLines(key, width = 96, maxlines = lines,\
                                flags = DF_WRAP_ELLIPSIS)
    // find out just how big this has to be
    var/size = namefont.RoundUp32(namefont.GetWidth(txt))
    var/icon/I = namefont.DrawText(txt, size / 2, 0,\
                     width = size, maxlines = lines,\
                     flags = DF_JUSTIFY_CENTER,\
                     icons_x = size / 32, icons_y = 1)
    var/obj/O = new
    O.pixel_y = -32
    O.icon = I
    overlays = list()   // reset overlays
    for(var/xx = 0, xx < I.setwidth, ++xx)
      O.icon_state = "[xx],0"
      O.pixel_x = (xx + (1 - I.setwidth) / 2) * 32
      overlays += O
    del(O)

    Logout()
        overlays = list()

Most names should fit nicely within the limits. Arial at 7 points is 11 pixels high, which is just a fraction too tall to fit 3 lines--so it will fit 2 lines, which is a good amount. There's not much point letting the name get huge, anyway.

You may find that white isn't the ideal color for the label. You can use the techniques discussed earlier to color it in. One quick change is to use DFP_Outline() to outline the name.

I.DFP_Outline(rgb(255, 255, 255), rgb(0, 0, 0))

You can also use QuickName() easily create the same overlays. This works the same way in DmiFontsPlus as in the original DmiFonts library.

More To Come

I'll have more examples in a future version of this documentation.

Reference

Datums and Procs

dmifont
This datum is a single font. Use it to render text or test the size of text to draw.

var/name
The name of the font.
var/height
The height of a line. This is equal to ascent + descent.
var/ascent
Distance from the top of a line to the baseline of text. This includes some whitespace.
var/descent
Distance from the bottom of a line to the baseline of text. This includes some whitespace.
var/avgwidth
The average width of a character.
var/maxwidth
The widest character width.
var/overhang
Extra width, such as from italics, for a line.
var/inleading
Internal leading vertical space, for accent marks.
var/exleading
External leading vertical space, just plain blank.
var/defchar
Default character (for characters not defined in the font).
var/start
First character in the font. (Do not change)
var/end
Last character in the font. (Do not change)
var/icon
The icon file used for this font.
var/antialias = 1
The antialiasing level for this font. Changing this var does nothing.
var/list/metrics
A list of character widths in groups of 3, starting with the first character (start). A filler value is placed at the end; although not used, it tells how many unique characters are in the font. This was added to improve speed.
var/list/defined
A list telling which characters are defined in the font, and which are not, ranging from 1 to 255. Undefined characters are replaced with the default character.
var/sizex = 1
Width of this font, in number of icons.
var/sizey = 1
Height of this font, in number of icons.
proc/GetWidth(text, flags=0, firstline=0)
Get the width of a line of text. If more than one line is used, this returns the width of the longest line. Flags may be included, although only DF_INCLUDE_AC (see DrawText()) is recognized. An indent may be specified for firstline, which is applied to the width of the first line only.
proc/GetCharAWidth(charcode)
Get the "A" width of a character: The amount to move right before drawing it.
proc/GetCharBWidth(charcode)
Get the "B" width of a character: The amount to move right while drawing it.
proc/GetCharCWidth(charcode)
Get the "C" width of a character: The amount to move right after drawing it.
proc/GetCharWidth(charcode)
Get the total A+B+C width of a character.
proc/GetLineUpTo(text, xlimit, index=1, ellipsis, flags)
Starting at index, find the next convenient place in text to mark a line break before reaching xlimit width. The ellipsis flag will include a trailing "..." in the final width, if set. The breakfirst var indicates that this text began on a break point, and may break immediately if necessary. (flags is used internally, and has replaced the old breakfirst argument from version 1.)
Note: The DF_INCLUDE_AC flag is ignored for a trailing ellipsis, if any; that is, the last dot's "C" width will not be counted against the total width.
proc/GetNextPosition(lastlines, nexttext, dmifont/nextfont, lastindent=0, flags=0)
Return the x position (0 is flush left) where, after drawing several lines of text using lastlines, more text may be drawn--possibly in another font. lastindent is the indent of the first line of lastlines, which may be useful information. This can be used to string several styles together, and it is used exactly that way in GetMultiFontLines().
proc/CountLines(text)
Count forced line breaks, represented by \n, in a text string.
proc/CountLinesConstrained(text, width=-1, flags=0, firstline=0)
Count lines, as they would be counted in DrawText().
proc/GetWidthConstrained(text, width=-1, flags=0, firstline=0, maxlines=-1)
Get the maximum width of the lines in text, as it would be shown in DrawText() with the same parameters.
proc/GoodBreaks(text, width=-1, flags=0, firstline=0)
Returns 1 if drawing text via DrawText() with the same parameters would break up text at good places, or 0 if it would have to split up a word. This is a good way to tell if a longer width would be preferred for some text.
proc/WillFit(text, width=-1, flags=0, firstline=0, maxlines=-1)
Returns 1 if drawing text via DrawText() with the same parameters would fit well, without any bad break points and without running out of room.
proc/GetLines(text, width=-1, flags=0, firstline=0, maxlines=-1, list/leftover)
Get a modified version of text broken into lines as they would be shown in DrawText() with the same parameters.
proc/GetCutoffIndex(text, width=-1, flags=0, firstline=0, maxlines=-1)
Returns the index in text where the next line would start off, after the text that would be displayed in DrawText(). Where GetLines() would give you some text to work with, copytext(text, GetCutoffIndex(...)) could pick up where GetLines() left off.
proc/GetLine(text, index=1)
Get a complete line of text starting at index and ending at either the end of the string or the first \n found.
proc/GetLastLineIndex(text, index=1)
Find the index of the beginning of the last line in a block of text, just after a \n character.
proc/GetNextIndex(text, index)
Following a break point (found by GetLineUpTo() or by searching for \n), find a suitable index to begin the next line.
proc/RoundUp32(n)
Round n up to the next highest multiple of 32. This is a good way to tell how many icons will be needed to fit a piece of text.
proc/SyncWidth(firstchar, lastchar)
Create a modified copy of this font to make a range of characters monospaced. Supply firstchar and lastchar as character codes (such as 48 and 57 for all digits), and the proc will return the new font. This is a good way to display scores, clocks, etc. Use the DF_INCLUDE_AC flag when rendering text in this format, unless the first character of your text (or the last, if right-justified, or both if center- or full-justified) was not modified. See DrawText() below for an explanation of the flag.
proc/DrawText(text, x, y, width=-1, flags=0, firstline=0, maxlines=-1, icons_x=0, icons_y=0, icon/drawover, list/leftover)

Draw text at position x,y in an /icon. x is the distance from the left edge, and y is from the top. The coordinates are for the upper left edge of the text. (If you want to draw from the baseline, subtract ascent from y.) The drawing area is an /icon datum returned by this proc. Text is white on a transparent background, but you can change it using the procs in /icon.

If you specify icons_x or icons_y you can restrict the /icon to a particular size (in tiles, not pixels), or it will expand to fit the text.

The width argument is the maximum width you will allow for your text, or -1 (the default) for as much width as possible. You can also use firstline to specify an indentation for the first line.

You can limit text to a number of lines with maxlines, or leave maxlines set to -1 for unlimited lines.

The flags argument allows you to decide how you want your text wrapped or justified. Possible flag values are:

  • DF_WRAP: Word-wrap text to fit. (default)
  • DF_WRAP_NONE: Do not wrap text.
  • DF_WRAP_ELLIPSIS: Wrap text. If there is more text than can be printed within maxlines, put an ellipsis (...) at the end of the last line to show there is more.
  • DF_WRAP_ONELINE: Do not wrap each line, but constrain it to width and use ... at the end to show there is more.
  • DF_JUSTIFY_LEFT: Left-justify text. (default)
  • DF_JUSTIFY_RIGHT: Right-justify text.
  • DF_JUSTIFY_CENTER: Center text.
  • DF_JUSTIFY: Justify text to both margins.
  • DF_INCLUDE_AC: Include the "before" (A) and "after" (C) widths of the beginning and end characters on each line, respectively.
  • DF_SET_WIDTH: Round width up to the nearest multiple of 32, or if not constrained find width first and then round up. Also adjust x if right- or center-justified so position 0 is at the center or right edge. This flag is meaningless to GetLines().
  • DF_NO_FORMAT: Do not format this text by calling GetLines(); assume GetLines() was already called in advance.

By adding flag values together or using the | operator on them, you can use different combinations of word wrapping and justification.

The drawover var is an /icon to draw on top of. This may be preferable to creating a new /icon and adding it to another one.

If you supply a list for leftover, it will be cleared out and filled with the rest of the text (if any) that didn't get drawn. If its length is 0 afterward, all of the text could be drawn to the constraints specified.

proc/DrawChar(charcode, x, y, icon/ic, charwidth=maxwidth)
Draw a character at position x,y in the icon.
charwidth is equivalent to GetCharBWidth(charcode), but is passed by DrawText() for the sake of speed.
proc/GetLinesMultiFont(list/items, width=-1, flags=0, firstline=0, maxheight=-1, list/leftover)
Used internally by DrawTextMultiFont(). Returns a /dmifonttextline datum, which is the first in a double-linked list. If a list is supplied for leftover, it will be cleared out and filled with any lines and font instructions that didn't get converted to datums because of height restrictions.
proc/WillFitMultiFont(list/items, width=-1, flags=0, firstline=0, maxheight=-1)
Returns 1 if drawing text via DrawTextMultiFont() with the same parameters would fit well, without any bad break points and without running out of room.
proc/DrawTextMultiFont(list/items, x, y, width=-1, flags=0, firstline=0, maxheight=-1, icons_x, icons_y, icon/drawover, list/leftover)

Draws a block of text in changing fonts. Most of the arguments are the same as in DrawText(), except for a few:

items is a list of text and fonts to draw, starting in this font (src). The proc will run down through the list and draw text or change fonts as requested. If a font is found, that font is used for subsequent text. If null is found, src becomes the default font again. An items list might look like this:

list("This is ", boldfont, "bold", null, " text!")

The items var can also be given a /dmifonttextline datum, for those crazy enough to work with it manually.

maxheight is the maximum height of all lines. Since the fonts may vary in size, maxlines wouldn't be appropriate.

If you supply a list for leftover, it will be cleared out and filled with items from the items list that didn't get drawn. The list can be used for a future call to this proc to display the rest.

proc/KeyToBreakable(text)
Breaks up a string of text with soft-break characters at appropriate points. This is designed for player keys but can be used with any text. The rules for inserting soft-breaks are:
  1. Do not add breaks around any spaces or other break chars.
  2. Add a hyphen break between a digit (0-9) and a non-digit.
  3. Add a hyphen break between a lowercase letter and an uppercase letter that immediately follows it.
  4. Add a soft break between a punctuation character and a non-punctuation character that immediately follows it.
proc/QuickName(atom/A, txt, color="#fff", outline, top, size=3, layer=FLY_LAYER)
Creates a set of overlays for an atom (usually a mob) to represent a name. By default this will appear below the atom unless you use a nonzero value for top. outline is the color of an optional outline, or use any nonzero, nontext value for a black outline. size is the number of icons in width the name may be; at most it can be 8. You may also specify the layer.
proc/QuickText(atom/A, txt, color="#fff", outline, x=0, y=0, bottom, justify=DF_JUSTIFY_LEFT, layer=FLY_LAYER)
Creates a set of overlays for an atom, usually an obj such as in a HUD. At most your text should only be 128 pixels long. x counts the number of pixels from the left, or right if right-justified; centered text ignores x. By default y counts the number of pixels from the top, unless you use a nonzero value for bottom. outline is the color of an optional outline (or any nonzero, nontext value for black), which will offset the text to fit. Text may be left-justified, centered, or right-justified by using the appropriate justify flags; the overlays will start on the target atom A and move off to the right and/or left as needed. You may also specify the layer.
dmifonttextline
Internal use only. Screw around with it at your own peril.
icon

These are the alterations to the standard /icon datum.

var/setwidth
Width, in tiles.
var/setheight
Height, in tiles.
proc/BlendIcon(icon/c, x, y, operation=ICON_OR, icon_width=32, icon_height=32)
Blends a single-tiled icon at offset position (x,y) onto this big icon.
icon_width and icon_height specify the maximum width of the icon from its upper left corner. They are not not used as hard limits, but rather to keep from doing unnecessary work on other icons in the set.
proc/DFP_Dilate()
"Dilates" the total image by adding copies shifted north, south, east and west by one pixel. Very useful for creating outlines of text.
proc/DFP_Outline(in_color, out_color)
Converts the icon from white-on-transparent to outlined text with the colors you supply.

Version History

Version 1.0: October 2008