# 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 intrude 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: Float # 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: Float # Pixels from the top of the line to the base of the characters var base: Float # Width of the texture var scale_w: Float # Height of the texture var scale_h: Float # 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, Float] # Additional distance between `prev_char` and `char` fun kerning(prev_char: nullable Char, char: Char): Float do if prev_char == null then return 0.0 return kernings[prev_char, char] or else 0.0 end 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: Float # Subtexture top coordinate var y: Float # Subtexture width var width: Float # Subtexture height var height: Float # Drawing offset on X var xoffset: Float # Drawing offset on Y var yoffset: Float # Cursor advance after drawing this character var xadvance: Float # 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.0 # assert fnt.kernings['A', 'C'] == -1.0 # 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_f, info_map["bold"] == "1", info_map["italic"] == "1", info_map["unicode"] == "1", info_map["padding"], info_map["spacing"], common_map["lineHeight"].to_f, common_map["base"].to_f, common_map["scaleW"].to_f, common_map["scaleH"].to_f ) # 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_f, attributes["y"].to_f, attributes["width"].to_f, attributes["height"].to_f, attributes["xoffset"].to_f, attributes["yoffset"].to_f, attributes["xadvance"].to_f, 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_f 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 text_width = 0.0 var max_width = text_sprites.max_width var max_height = text_sprites.max_height var line_height = desc.line_height var partial_line_skip = line_height * partial_line_mod.to_f var line_sprites = new Array[Sprite] var prev_char = null var i = -1 while i < text.length - 1 do i += 1 var c = text[i] # Special characters var word_break = false if c == '\n' then justify(line_sprites, text_sprites.align, dx) dy -= line_height if max_height != null and max_height < -dy + line_height then break dx = 0.0 prev_char = null continue else if c == pld then dy -= partial_line_skip word_break = true continue else if c == plu then dy += partial_line_skip word_break = true continue else if c.is_whitespace then var space_advance = if desc.chars.keys.has(' ') then desc.chars[' '].xadvance else if desc.chars.keys.has('f') then desc.chars['f'].xadvance else 16.0 dx += space_advance word_break = true end # End of a word? if word_break then # If we care about line width, check for overflow if max_width != null then # Calculate the length of the next word var prev_w = null var word_len = 0.0 for wi in [i+1..text.length[ do var w = text[wi] if w == '\n' or w == pld or w == plu or w.is_whitespace then break word_len += advance(prev_w, w) prev_w = w end # Would the line be too long? if dx + word_len > max_width then if text_sprites.wrap then # Wrap justify(line_sprites, text_sprites.align, dx) dy -= line_height if max_height != null and max_height < -dy + line_height then break dx = 0.0 else # Cut short justify(line_sprites, text_sprites.align, dx) dy -= line_height if max_height != null and max_height < -dy + line_height then break dx = 0.0 while c != '\n' and i < text.length - 1 do i += 1 c = text[i] end end end end 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 var kerning = desc.kerning(prev_char, c) var x = dx + char_info.width/2.0 + char_info.xoffset + kerning var y = dy - char_info.height/2.0 - char_info.yoffset var pos = text_sprites.anchor.offset(x, y, 0.0) var s = new Sprite(char_info.subtexture, pos) text_sprites.sprites.add s line_sprites.add s dx += advance + kerning prev_char = c text_width = text_width.max(dx) end justify(line_sprites, text_sprites.align, dx) # valign if text_sprites.valign != 0.0 then var d = (-dy+line_height) * text_sprites.valign for s in text_sprites.sprites do s.center.y += d end text_sprites.width = text_width.max(dx) text_sprites.height = dy + line_height end # Character replacing other characters 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 private fun advance(prev_char: nullable Char, char: Char): Float do var char_info = desc.chars[char] var kerning = desc.kerning(prev_char, char) return char_info.xadvance + kerning end private fun justify(line_sprites: Array[Sprite], align: Float, line_width: Float) do var dx = -line_width*align for s in line_sprites do s.center.x += dx line_sprites.clear end end