1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # Parse Angel Code BMFont format and draw text
17 # The BMFont format supports packed textures, varying advance per character and
18 # even kernings. It can be generated with a number of tools, inluding:
19 # * BMFont, free software Windows app, http://www.angelcode.com/products/bmfont/
20 # * Littera, a web app, http://kvazars.com/littera/
22 # Format reference: http://www.angelcode.com/products/bmfont/doc/file_format.html
29 # BMFont description, parsed with `Text::parse_bmfont` or loaded as a `BMFontAsset`
31 # This class flattens all the `info` and `common` data.
37 # How the font was generated.
39 # Name of the source true type font
42 # Size of the source true type font
51 # Does the font uses the Unicode charset?
54 # Padding for each character
56 # In the format `up,right,down,left`
59 # Spacing for each character
61 # In the format `horizontal,vertical`.
67 # Information common to all characters
69 # Distance in pixels between each line of text
70 var line_height
: Float
72 # Pixels from the top of the line to the base of the characters
75 # Width of the texture
78 # Height of the texture
82 var pages
= new Map[String, TextureAsset]
84 # Characters in the font
85 var chars
= new Map[Char, BMFontChar]
87 # Distance between certain characters
88 var kernings
= new HashMap2[Char, Char, Float]
90 # Additional distance between `prev_char` and `char`
91 fun kerning
(prev_char
: nullable Char, char
: Char): Float
93 if prev_char
== null then return 0.0
94 return kernings
[prev_char
, char
] or else 0.0
97 redef fun to_s
do return "<{class_name} {face} at {size} pt, "+
98 "{pages.length} pages, {chars.length} chars>"
117 # Description of a character in a `BMFont`
120 # Subtexture left coordinate
123 # Subtexture top coordinate
132 # Drawing offset on X
135 # Drawing offset on Y
138 # Cursor advance after drawing this character
141 # Full texture contaning this character and others
142 var page
: TextureAsset
144 # TODO Channel where the image is found
147 # Subtexture with this character image only
148 var subtexture
: Texture = page
.subtexture
(x
, y
, width
, height
) is lazy
153 # Parse `self` as an XML BMFont description file
155 # Reports only basic XML format errors, other errors may be ignored or
161 # <info face="arial" size="72" bold="0" italic="0" charset=""
162 # unicode="1" stretchH="100" smooth="1" aa="1" padding="2,2,2,2"
163 # spacing="0,0" outline="0"/>
164 # <common lineHeight="80" base="65" scaleW="4030" scaleH="231"
165 # pages="1" packed="0"/>
167 # <page id="0" file="arial.png"/>
170 # <char id="65" x="2519" y="10" width="55" height="59" xoffset="0"
171 # yoffset="13" xadvance="48" page="0" chnl="15"/>
172 # <char id="66" x="2600" y="10" width="46" height="58" xoffset="5"
173 # yoffset="13" xadvance="48" page="0" chnl="15"/>
174 # <char id="67" x="2673" y="9" width="52" height="60" xoffset="4"
175 # yoffset="12" xadvance="52" page="0" chnl="15"/>
177 # <kernings count="1">
178 # <kerning first="65" second="67" amount="-1"/>
183 # var fnt = desc.parse_bmfont("dir_in_assets").value
184 # assert fnt.to_s == "<BMFont arial at 72.0 pt, 1 pages, 3 chars>"
185 # assert fnt.line_height == 80.0
186 # assert fnt.kernings['A', 'C'] == -1.0
187 # assert fnt.chars['A'].page.path == "dir_in_assets/arial.png"
189 fun parse_bmfont
(dir
: String): MaybeError[BMFont, Error]
193 if xml
isa XMLError then
194 var msg
= "XML Parse Error: {xml.message}:{xml.location or else 0}"
195 return new MaybeError[BMFont, Error](maybe_error
=new Error(msg
))
199 var roots
= xml
["font"]
200 if roots
.is_empty
then
201 var msg
= "Error: the XML document doesn't declare the expected `font` root"
202 return new MaybeError[BMFont, Error](maybe_error
=new Error(msg
))
205 # Expect the rest of the document to be well formatted
206 var root
= roots
.first
208 var info
= root
["info"].first
209 assert info
isa XMLAttrTag
210 var info_map
= info
.attributes_to_map
212 var common
= root
["common"].first
213 assert common
isa XMLAttrTag
214 var common_map
= common
.attributes_to_map
216 var fnt
= new BMFont(
218 info_map
["size"].to_f
,
219 info_map
["bold"] == "1",
220 info_map
["italic"] == "1",
221 info_map
["unicode"] == "1",
224 common_map
["lineHeight"].to_f
,
225 common_map
["base"].to_f
,
226 common_map
["scaleW"].to_f
,
227 common_map
["scaleH"].to_f
230 # Pages / pixel data files
231 var xml_pages
= root
["pages"].first
232 for page
in xml_pages
["page"] do
233 if not page
isa XMLAttrTag then continue
235 var attributes
= page
.attributes_to_map
236 var file
= dir
/ attributes
["file"]
237 fnt
.pages
[attributes
["id"]] = new TextureAsset(file
)
241 for item
in root
["chars"].first
["char"] do
242 if not item
isa XMLAttrTag then continue
244 var attributes
= item
.attributes_to_map
245 var id
= attributes
["id"].to_i
.code_point
247 var c
= new BMFontChar(
248 attributes
["x"].to_f
, attributes
["y"].to_f
,
249 attributes
["width"].to_f
, attributes
["height"].to_f
,
250 attributes
["xoffset"].to_f
, attributes
["yoffset"].to_f
,
251 attributes
["xadvance"].to_f
,
252 fnt
.pages
[attributes
["page"]])
257 # Kerning between two characters
258 var kernings
= root
["kernings"]
259 if kernings
.not_empty
then
260 for item
in kernings
.first
["kerning"] do
261 if not item
isa XMLAttrTag then continue
263 var attributes
= item
.attributes_to_map
264 var first
= attributes
["first"].to_i
.code_point
265 var second
= attributes
["second"].to_i
.code_point
266 var amount
= attributes
["amount"].to_f
267 fnt
.kernings
[first
, second
] = amount
271 return new MaybeError[BMFont, Error](fnt
)
275 # BMFont from the assets folder
279 # var font = new BMFontAsset("arial.fnt")
280 # var ui_text = new TextSprites(font, ui_camera.top_left)
282 # redef fun on_create
287 # assert font.error == null
289 # ui_text.text = "Hello world!"
299 # Require: `error == null`
303 var cache
= desc_cache
304 if cache
!= null then return cache
306 assert error
== null else print_error error
308 # Load on first access
311 assert error
== null else print_error error
313 return desc_cache
.as(not null)
316 private var desc_cache
: nullable BMFont = null
319 var error
: nullable Error = null
321 # XML description in the assets folder
322 private var text_asset
= new TextAsset(path
) is lateinit
324 # Load font description and textures from the assets folder
326 # Sets `error` if an error occurred, otherwise
327 # the font description can be accessed via `desc`.
330 var text_asset
= text_asset
332 var error
= text_asset
.error
333 if error
!= null then
338 var desc
= text_asset
.to_s
339 var fnt_or_error
= desc
.parse_bmfont
(path
.dirname
)
340 if fnt_or_error
.is_error
then
341 self.error
= fnt_or_error
.error
345 var fnt
= fnt_or_error
.value
346 self.desc_cache
= fnt
349 for page_name
, texture
in fnt
.pages
do
352 # Move up any texture loading error.
353 # This algo keeps only the latest error,
354 # but this isn't a problem on single page fonts.
355 error
= texture
.error
356 if error
!= null then self.error
= error
360 redef fun write_into
(text_sprites
, text
)
366 var max_width
= text_sprites
.max_width
367 var max_height
= text_sprites
.max_height
369 var line_height
= desc
.line_height
370 var partial_line_skip
= line_height
* partial_line_mod
.to_f
371 var line_sprites
= new Array[Sprite]
375 while i
< text
.length
- 1 do
380 var word_break
= false
382 justify
(line_sprites
, text_sprites
.align
, dx
)
384 if max_height
!= null and max_height
< -dy
+ line_height
then break
388 else if c
== pld
then
389 dy
-= partial_line_skip
392 else if c
== plu
then
393 dy
+= partial_line_skip
396 else if c
.is_whitespace
then
397 var space_advance
= if desc
.chars
.keys
.has
(' ') then
398 desc
.chars
[' '].xadvance
399 else if desc
.chars
.keys
.has
('f') then
400 desc
.chars
['f'].xadvance
408 # If we care about line width, check for overflow
409 if max_width
!= null then
410 # Calculate the length of the next word
413 for wi
in [i
+1..text
.length
[ do
416 if w
== '\n' or w
== pld
or w
== plu
or w
.is_whitespace
then break
417 word_len
+= advance
(prev_w
, w
)
421 # Would the line be too long?
422 if dx
+ word_len
> max_width
then
423 if text_sprites
.wrap
then
425 justify
(line_sprites
, text_sprites
.align
, dx
)
427 if max_height
!= null and max_height
< -dy
+ line_height
then break
431 justify
(line_sprites
, text_sprites
.align
, dx
)
433 if max_height
!= null and max_height
< -dy
+ line_height
then break
435 while c
!= '\n' and i
< text
.length
- 1 do
447 # Replace or skip unknown characters
448 if not desc
.chars
.keys
.has
(c
) then
449 var rc
= replacement_char
450 if rc
== null then continue
454 var char_info
= desc
.chars
[c
]
455 var advance
= char_info
.xadvance
456 var kerning
= desc
.kerning
(prev_char
, c
)
458 var x
= dx
+ char_info
.width
/2.0 + char_info
.xoffset
+ kerning
459 var y
= dy
- char_info
.height
/2.0 - char_info
.yoffset
460 var pos
= text_sprites
.anchor
.offset
(x
, y
, 0.0)
461 var s
= new Sprite(char_info
.subtexture
, pos
)
462 text_sprites
.sprites
.add s
465 dx
+= advance
+ kerning
468 text_width
= text_width
.max
(dx
)
471 justify
(line_sprites
, text_sprites
.align
, dx
)
474 if text_sprites
.valign
!= 0.0 then
475 var d
= (-dy
+line_height
) * text_sprites
.valign
476 for s
in text_sprites
.sprites
do s
.center
.y
+= d
479 text_sprites
.width
= text_width
.max
(dx
)
480 text_sprites
.height
= dy
+ line_height
483 # Character replacing other characters missing from the font
484 private var replacement_char
: nullable Char is lazy
do
485 for c
in "�?".chars
do
486 if desc
.chars
.keys
.has
(c
) then return c
491 private fun advance
(prev_char
: nullable Char, char
: Char): Float
493 var char_info
= desc
.chars
[char
]
494 var kerning
= desc
.kerning
(prev_char
, char
)
495 return char_info
.xadvance
+ kerning
498 private fun justify
(line_sprites
: Array[Sprite], align
: Float, line_width
: Float)
500 var dx
= -line_width
*align
501 for s
in line_sprites
do s
.center
.x
+= dx