From: Alexis Laferrière Date: Mon, 22 May 2017 18:12:42 +0000 (-0700) Subject: gamnit: intro BMFont parser and drawing services X-Git-Url: http://nitlanguage.org gamnit: intro BMFont parser and drawing services Signed-off-by: Alexis Laferrière --- diff --git a/lib/gamnit/bmfont.nit b/lib/gamnit/bmfont.nit new file mode 100644 index 0000000..4abdaa5 --- /dev/null +++ b/lib/gamnit/bmfont.nit @@ -0,0 +1,422 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Parse Angel Code BMFont format and draw text +# +# The BMFont format supports packed textures, varying advance per character and +# even kernings. It can be generated with a number of tools, inluding: +# * BMFont, free software Windows app, http://www.angelcode.com/products/bmfont/ +# * Littera, a web app, http://kvazars.com/littera/ +# +# Format reference: http://www.angelcode.com/products/bmfont/doc/file_format.html +module bmfont + +private import dom + +import font + +# BMFont description, parsed with `Text::parse_bmfont` or loaded as a `BMFontAsset` +# +# This class flattens all the `info` and `common` data. +class BMFont + + # --- + # info part + # + # How the font was generated. + + # Name of the source true type font + var face: Text + + # Size of the source true type font + var size: Int + + # Is the font bold? + var bold: Bool + + # Is the font italic? + var italic: Bool + + # Does the font uses the Unicode charset? + var unicode: Bool + + # Padding for each character + # + # In the format `up,right,down,left` + var padding: String + + # Spacing for each character + # + # In the format `horizontal,vertical`. + var spacing: String + + # --- + # common part + # + # Information common to all characters + + # Distance in pixels between each line of text + var line_height: Int + + # Pixels from the top of the line to the base of the characters + var base: Int + + # Width of the texture + var scale_w: Int + + # Height of the texture + var scale_h: Int + + # Textures + var pages = new Map[String, TextureAsset] + + # Characters in the font + var chars = new Map[Char, BMFontChar] + + # Distance between certain characters + var kernings = new HashMap2[Char, Char, Int] + + redef fun to_s do return "<{class_name} {face} at {size} pt, "+ + "{pages.length} pages, {chars.length} chars>" + + # TODO + # + # # From info + # charset + # stretchH + # smooth + # aa + # outline + # + # # From common + # packed + # alphaChnl + # redChnl + # greenChnl + # blueChnl +end + +# Description of a character in a `BMFont` +class BMFontChar + + # Subtexture left coordinate + var x: Int + + # Subtexture top coordinate + var y: Int + + # Subtexture width + var width: Int + + # Subtexture height + var height: Int + + # Drawing offset on X + var xoffset: Int + + # Drawing offset on Y + var yoffset: Int + + # Cursor advance after drawing this character + var xadvance: Int + + # Full texture contaning this character and others + var page: TextureAsset + + # TODO Channel where the image is found + #var chnl: Int + + # Subtexture with this character image only + var subtexture: Texture = page.subtexture(x, y, width, height) is lazy +end + +redef class Text + + # Parse `self` as an XML BMFont description file + # + # Reports only basic XML format errors, other errors may be ignored or + # cause a crash. + # + # ~~~ + # var desc = """ + # + # + # + # + # + # + # + # + # + # + # + # + # + # + # + # """ + # + # var fnt = desc.parse_bmfont("dir_in_assets").value + # assert fnt.to_s == "" + # assert fnt.line_height == 80 + # assert fnt.kernings['A', 'C'] == -1 + # assert fnt.chars['A'].page.path == "dir_in_assets/arial.png" + # ~~~ + fun parse_bmfont(dir: String): MaybeError[BMFont, Error] + do + # Parse XML + var xml = to_xml + if xml isa XMLError then + var msg = "XML Parse Error: {xml.message}:{xml.location or else 0}" + return new MaybeError[BMFont, Error](maybe_error=new Error(msg)) + end + + # Basic sanity check + var roots = xml["font"] + if roots.is_empty then + var msg = "Error: the XML document doesn't declare the expected `font` root" + return new MaybeError[BMFont, Error](maybe_error=new Error(msg)) + end + + # Expect the rest of the document to be well formatted + var root = roots.first + + var info = root["info"].first + assert info isa XMLAttrTag + var info_map = info.attributes_to_map + + var common = root["common"].first + assert common isa XMLAttrTag + var common_map = common.attributes_to_map + + var fnt = new BMFont( + info_map["face"], + info_map["size"].to_i, + info_map["bold"] == "1", + info_map["italic"] == "1", + info_map["unicode"] == "1", + info_map["padding"], + info_map["spacing"], + common_map["lineHeight"].to_i, + common_map["base"].to_i, + common_map["scaleW"].to_i, + common_map["scaleH"].to_i + ) + + # Pages / pixel data files + var xml_pages = root["pages"].first + for page in xml_pages["page"] do + if not page isa XMLAttrTag then continue + + var attributes = page.attributes_to_map + var file = dir / attributes["file"] + fnt.pages[attributes["id"]] = new TextureAsset(file) + end + + # Char description + for item in root["chars"].first["char"] do + if not item isa XMLAttrTag then continue + + var attributes = item.attributes_to_map + var id = attributes["id"].to_i.code_point + + var c = new BMFontChar( + attributes["x"].to_i, attributes["y"].to_i, + attributes["width"].to_i, attributes["height"].to_i, + attributes["xoffset"].to_i, attributes["yoffset"].to_i, + attributes["xadvance"].to_i, + fnt.pages[attributes["page"]]) + + fnt.chars[id] = c + end + + # Kerning between two characters + var kernings = root["kernings"] + if kernings.not_empty then + for item in kernings.first["kerning"] do + if not item isa XMLAttrTag then continue + + var attributes = item.attributes_to_map + var first = attributes["first"].to_i.code_point + var second = attributes["second"].to_i.code_point + var amount = attributes["amount"].to_i + fnt.kernings[first, second] = amount + end + end + + return new MaybeError[BMFont, Error](fnt) + end +end + +# BMFont from the assets folder +# +# ~~~ +# redef class App +# var font = new BMFontAsset("arial.fnt") +# var ui_text = new TextSprites(font, ui_camera.top_left) +# +# redef fun on_create +# do +# super +# +# font.load +# assert font.error == null +# +# ui_text.text = "Hello world!" +# end +# end +# ~~~ +class BMFontAsset + super Asset + super Font + + # Font description + # + # Require: `error == null` + fun desc: BMFont + do + # Cached results + var cache = desc_cache + if cache != null then return cache + var error = error + assert error == null else print_error error + + # Load on first access + load + error = self.error + assert error == null else print_error error + + return desc_cache.as(not null) + end + + private var desc_cache: nullable BMFont = null + + # Error at loading + var error: nullable Error = null + + # XML description in the assets folder + private var text_asset = new TextAsset(path) is lateinit + + # Load font description and textures from the assets folder + # + # Sets `error` if an error occurred, otherwise + # the font description can be accessed via `desc`. + fun load + do + var text_asset = text_asset + text_asset.load + var error = text_asset.error + if error != null then + self.error = error + return + end + + var desc = text_asset.to_s + var fnt_or_error = desc.parse_bmfont(path.dirname) + if fnt_or_error.is_error then + self.error = fnt_or_error.error + return + end + + var fnt = fnt_or_error.value + self.desc_cache = fnt + + # Load textures too + for page_name, texture in fnt.pages do + texture.load + + # Move up any texture loading error. + # This algo keeps only the latest error, + # but this isn't a problem on single page fonts. + error = texture.error + if error != null then self.error = error + end + end + + redef fun write_into(text_sprites, text) + do + var dx = 0.0 + var dy = 0.0 + + var line_height = desc.line_height.to_f + + var prev_char = null + for c in text do + + var partial_line_mod = 0.4 + + # Special characters + if c == '\n' then + dy -= line_height.to_f + dx = 0.0 #advance/2.0 + prev_char = null + continue + else if c == pld then + dy -= line_height * partial_line_mod.to_f + prev_char = null + continue + else if c == plu then + dy += line_height * partial_line_mod.to_f + prev_char = null + continue + else if c.is_whitespace then + var advance = if desc.chars.keys.has(' ') then + desc.chars[' '].xadvance.to_f + else if desc.chars.keys.has('f') then + desc.chars['f'].xadvance.to_f + else 16.0 + dx += advance + prev_char = null + continue + end + + # Replace or skip unknown characters + if not desc.chars.keys.has(c) then + var rc = replacement_char + if rc == null then continue + c = rc + end + + var char_info = desc.chars[c] + var advance = char_info.xadvance.to_f + + var kerning = 0.0 + if prev_char != null then + kerning = (desc.kernings[prev_char, c] or else 0).to_f + end + + var x = dx + char_info.width.to_f/2.0 + char_info.xoffset.to_f + kerning + var y = dy - char_info.height.to_f/2.0 - char_info.yoffset.to_f + var pos = text_sprites.anchor.offset(x, y, 0.0) + text_sprites.sprites.add new Sprite(char_info.subtexture, pos) + + dx += advance + kerning + prev_char = c + end + end + + # Character replacing other charactesr missing from the font + private var replacement_char: nullable Char is lazy do + for c in "�?".chars do + if desc.chars.keys.has(c) then return c + end + return null + end +end