Hummingbird loads fonts from local folders that can later be used during UI rendering. You should point the font folders to Hummingbird via the cohtml::System::AddFontsFromFolder API call. All fonts within the folder (but not sub-folders) will be available to Hummingbird. Only the fonts actually used will be loaded, so it's safe to point even to large libraries with many fonts, although this will slow down the initialization process as all fonts will be indexed.
During CSS authoring simply use the syntax:
You should only use fonts that have been enumerated. If a font is required that is missing, Hummingbird will try to use "Droid Sans". You should always have that font as a backup. You can use cohtml::System::AddDefaultFallbackFontName to specify which font you want to be used as backup font if one is not found.
Hummingbird supports TrueType (.ttf/.ttc) and OpenType (.otf) fonts.
Hummingbird also supports @font-face declarations in CSS. However, there are some caveats and differences from the HTML Standard. Fonts loaded with @font-face are global to the whole system and are not unloaded after changing pages. This means that @font-face loaded from one View can affect text in another View. @font-face from one page can also affect next page that is loaded after following a link or changing the URL. The specified font is kept loaded and the registered font-family cannot be overwritten. If you have a second page that has a font-family rule with the same name and different TTF URL it will not be used, but the old @font-face style will remain in effect. We are keeping the font alive because it is common behavior in games to use the same fonts across all pages and this saves loading and unloading the same font. Hummingbird requires exact match to use @font-face rule. This means that only text in <i> tag will match @font-face with font-style:italics. Text in <i> with regular font will not match.
Hummingbird draws text through a font atlas. If a character is encountered that is not in the font atlas, it is rasterized on-the-fly. Larger fonts in Hummingbird are drawn with distance fields and provide very high-performance animation and very low memory usage.
By default Hummingbird uses FreeType for rendering fonts and manually reads .ttf / .otf files. Hummingbird can also work with Bitmap Font with bitmaps supplied by the client.
To use bitmap fonts, call cohtml::System::AddBitmapFont providing a thorough description of the font and bitmaps.
This method has 2 overloads:
On frontend side, using bitmap fonts doesn't differ much from what be otherwise required - just tell Hummingbird to use the font family you just loaded:
One of the most popular tools for generating bitmap fonts is BMFont by AngelCode. Here is an example how to parse a binary .fnt file and load the font in Hummingbird.
// BMFont Parsing
#pragma pack(push, 1)
struct BMFontBlock
{
uint8_t Type;
uint32_t Size;
};
struct BMFontBlockInfo
{
int16_t FontSize;
uint8_t BitField; //bit 0: smooth, bit 1 : unicode, bit 2 : italic, bit 3 : bold, bit 4 : fixedHeigth, bits 5 - 7 : reserved
uint8_t CharSet;
uint16_t StretchH;
uint8_t AA;
uint8_t PaddingUp;
uint8_t PaddingRight;
uint8_t PaddingDown;
uint8_t PaddingLeft;
uint8_t SpacingHoriz;
uint8_t SpacingVert;
uint8_t Outline;
// char [n + 1] FontName. Null terminated string
};
struct BMFontBlockCommon
{
uint16_t LineHeight;
uint16_t BaseLine;
uint16_t ScaleW;
uint16_t ScaleH;
uint16_t Pages;
uint8_t BitField; // bits 0-6: reserved, bit 7: packed
uint8_t AlphaChannel;
uint8_t RedChannel;
uint8_t GreenChannel;
uint8_t BlueChannel;
};
// BMFontBlockPages
// PageNames p * (n + 1), p-pages,each with n-length
// Repeated until all chars are described
// NumberOfChars = BMFontBlock.Size / 20
struct BMFontBlockChars
{
uint32_t Id;
uint16_t X;
uint16_t Y;
uint16_t Width;
uint16_t Height;
int16_t XOffset;
int16_t YOffset;
int16_t Advance;
uint8_t Page;
uint8_t Channel;
};
struct BMFontBlockKerningPairs
{
uint32_t First;
uint32_t Second;
int16_t Amount;
};
#pragma pack(pop)
struct BMFontData
{
std::string Name;
unsigned Size;
bool IsBold;
bool IsItalic;
unsigned Baseline;
unsigned LineHeight;
unsigned TextureWidth;
unsigned TextureHeight;
struct CharInfo
{
unsigned Id;
unsigned X;
unsigned Y;
unsigned Width;
unsigned Height;
int OffsetX;
int OffsetY;
unsigned Advance;
unsigned ImageIndex;
};
std::vector<CharInfo> Chars;
std::vector<std::string> ImageNames;
struct KerningInfo
{
unsigned FirstId;
unsigned SecondId;
int Amount;
};
std::vector<KerningInfo> Kerning;
};
bool ParseBMFontFile(const char* filename, BMFontData& data)
{
std::ifstream file(filename, std::ios::binary);
if (!file)
{
return false;
}
uint32_t magicBytes;
file.read(reinterpret_cast<char*>(&magicBytes), 4);
if (magicBytes != 0x03464d42) // BMF3 BMF is magic signature and 3 is the current version
{
return false;
}
// Assume there is one from each type of blocks
// Info block
BMFontBlock infoBlockSize;
file.read(reinterpret_cast<char*>(&infoBlockSize), sizeof(BMFontBlock));
if (infoBlockSize.Type != 1) // Check if it a info block
{
return false;
}
std::unique_ptr<char[]> infoBlockData = std::make_unique<char[]>(infoBlockSize.Size);
file.read(infoBlockData.get(), infoBlockSize.Size);
BMFontBlockInfo* infoBlock = reinterpret_cast<BMFontBlockInfo*>(infoBlockData.get());
data.Name = std::string(infoBlockData.get() + sizeof(BMFontBlockInfo));
data.Size = std::abs(infoBlock->FontSize);
data.IsBold = !!(infoBlock->BitField & (1 << 4));
data.IsItalic = !!(infoBlock->BitField & (1 << 5));
// Common block
BMFontBlock commonBlockSize;
file.read(reinterpret_cast<char*>(&commonBlockSize), sizeof(BMFontBlock));
if (commonBlockSize.Type != 2
|| commonBlockSize.Size != sizeof(BMFontBlockCommon))
{
return false;
}
BMFontBlockCommon commonBlock;
file.read(reinterpret_cast<char*>(&commonBlock), sizeof(BMFontBlockCommon));
data.LineHeight = commonBlock.LineHeight;
data.Baseline = commonBlock.BaseLine;
data.TextureWidth = commonBlock.ScaleW;
data.TextureHeight = commonBlock.ScaleH;
// Pages block
BMFontBlock pagesBlockSize;
file.read(reinterpret_cast<char*>(&pagesBlockSize), sizeof(BMFontBlock));
if (pagesBlockSize.Type != 3)
{
return false;
}
// Pages are commonBlock.Pages * (n + 1) bytes long. Each page is n byte string with 1 extra byte for terminating null
const auto pageStringLength = (pagesBlockSize.Size / commonBlock.Pages) - 1;
std::unique_ptr<char[]> pagesBlockData = std::make_unique<char[]>(pagesBlockSize.Size);
file.read(pagesBlockData.get(), pagesBlockSize.Size);
data.ImageNames.reserve(commonBlock.Pages);
for (auto i = 0; i < commonBlock.Pages; ++i)
{
data.ImageNames.push_back(pagesBlockData.get() + i * pageStringLength);
}
// Chars block
BMFontBlock charsBlockSize;
file.read(reinterpret_cast<char*>(&charsBlockSize), sizeof(BMFontBlock));
if (charsBlockSize.Type != 4)
{
return false;
}
const auto numChars = charsBlockSize.Size / sizeof(BMFontBlockChars);
std::unique_ptr<BMFontBlockChars[]> charsBlockData = std::make_unique<BMFontBlockChars[]>(numChars);
file.read(reinterpret_cast<char*>(charsBlockData.get()), charsBlockSize.Size);
data.Chars.reserve(numChars);
for (auto i = 0; i < numChars; ++i)
{
if (charsBlockData[i].Channel != 8 // Alpha Channel
&& charsBlockData[i].Channel != 15) // All Channels
{
return false;
}
BMFontData::CharInfo chInfo;
chInfo.Id = charsBlockData[i].Id;
chInfo.ImageIndex = charsBlockData[i].Page;
chInfo.X = charsBlockData[i].X;
chInfo.Y = charsBlockData[i].Y;
chInfo.Width = charsBlockData[i].Width;
chInfo.Height = charsBlockData[i].Height;
chInfo.OffsetX = charsBlockData[i].XOffset;
chInfo.OffsetY = charsBlockData[i].YOffset;
chInfo.Advance = charsBlockData[i].Advance;
data.Chars.push_back(chInfo);
}
// Kerning block
BMFontBlock kerningBlockSize;
file.read(reinterpret_cast<char*>(&kerningBlockSize), sizeof(BMFontBlock));
if (file)
{
if (kerningBlockSize.Type != 5)
{
return false;
}
const auto numKerning = kerningBlockSize.Size / sizeof(BMFontBlockKerningPairs);
std::unique_ptr<BMFontBlockKerningPairs[]> kerningBlockData = std::make_unique<BMFontBlockKerningPairs[]>(numKerning);
file.read(reinterpret_cast<char*>(kerningBlockData.get()), kerningBlockSize.Size);
data.Kerning.reserve(numKerning);
for (auto i = 0; i < numKerning; ++i)
{
BMFontData::KerningInfo kInfo;
kInfo.FirstId = kerningBlockData[i].First;
kInfo.SecondId = kerningBlockData[i].Second;
kInfo.Amount = kerningBlockData[i].Amount;
data.Kerning.push_back(kInfo);
}
}
return true;
}
After the .fnt file is parsed we need to initialize cohtml::BitmapFontDescription struct with the required data.
// Load Bitmap Font
BMFontData data;
if (ParseBMFontFile("BitmapFont.fnt", data))
{
std::vector<std::vector<char>> images;
std::vector<const char*> imagesData;
std::vector<unsigned> imagesSize;
for (auto i = 0; i < data.ImageNames.size(); ++i)
{
std::ifstream file(data.ImageNames[i], std::ios::binary);
if (!file)
{
return false;
}
std::vector<char> fileContents((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
images.push_back(std::move(fileContents));
imagesData.push_back(images.back().data());
imagesSize.push_back(unsigned(images.back().size()));
}
cohtml::BitmapFontDescription desc;
desc.FontFamily = data.Name.c_str();
desc.Size = data.Size;
desc.IsBold = data.IsBold;
desc.IsItalic = data.IsItalic;
desc.Baseline = data.Baseline;
desc.LineHeight = data.LineHeight;
std::vector<cohtml::BitmapFontDescription::CharInfo> descChars;
descChars.reserve(data.Chars.size());
for (const auto& ch : data.Chars)
{
cohtml::BitmapFontDescription::CharInfo info;
info.CharCode = ch.Id;
info.X = float(ch.X) / float(data.TextureWidth);
info.Y = float(ch.Y) / float(data.TextureWidth);
info.Width = float(ch.Width) / float(data.TextureWidth);
info.Height = float(ch.Height) / float(data.TextureWidth);
info.OffsetX = ch.OffsetX;
info.OffsetY = ch.OffsetY;
info.Advance = ch.Advance;
info.ImageIndex = ch.ImageIndex;
descChars.push_back(info);
}
desc.Chars = descChars.data();
desc.NumChars = unsigned(descChars.size());
std::vector<cohtml::BitmapFontDescription::KerningPairInfo> descKerning;
descKerning.reserve(data.Kerning.size());
for (const auto& ker : data.Kerning)
{
cohtml::BitmapFontDescription::KerningPairInfo info;
info.FirstCharCode = ker.FirstId;
info.SecondCharCode = ker.SecondId;
info.Amount = ker.Amount;
descKerning.push_back(info);
}
desc.KerningPairs = descKerning.data();
desc.NumKerningPairs = unsigned(descKerning.size());
m_UISystem->AddBitmapFont(imagesData.data(), imagesSize.data(), unsigned(images.size()), desc);
}