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 pos: Point3d[Float] = ui_camera.top_left.offset(128.0, -128.0, 0.0)
281 # var ui_text = new TextSprites(font, pos)
283 # redef fun on_create
288 # assert font.error == null
290 # ui_text.text = "Hello world!"
300 # Require: `error == null`
304 var cache
= desc_cache
305 if cache
!= null then return cache
307 assert error
== null else print_error error
309 # Load on first access
312 assert error
== null else print_error error
314 return desc_cache
.as(not null)
317 private var desc_cache
: nullable BMFont = null
320 var error
: nullable Error = null
322 # XML description in the assets folder
323 private var text_asset
= new TextAsset(path
) is lateinit
325 # Load font description and textures from the assets folder
327 # Sets `error` if an error occurred, otherwise
328 # the font description can be accessed via `desc`.
331 var text_asset
= text_asset
333 var error
= text_asset
.error
334 if error
!= null then
339 var desc
= text_asset
.to_s
340 var fnt_or_error
= desc
.parse_bmfont
(path
.dirname
)
341 if fnt_or_error
.is_error
then
342 self.error
= fnt_or_error
.error
346 var fnt
= fnt_or_error
.value
347 self.desc_cache
= fnt
350 for page_name
, texture
in fnt
.pages
do
353 # Move up any texture loading error.
354 # This algo keeps only the latest error,
355 # but this isn't a problem on single page fonts.
356 error
= texture
.error
357 if error
!= null then self.error
= error
361 redef fun write_into
(text_sprites
, text
)
367 var max_width
= text_sprites
.max_width
368 var max_height
= text_sprites
.max_height
370 var line_height
= desc
.line_height
371 var partial_line_skip
= line_height
* partial_line_mod
.to_f
372 var line_sprites
= new Array[Sprite]
376 while i
< text
.length
- 1 do
381 var word_break
= false
383 justify
(line_sprites
, text_sprites
.align
, dx
)
385 if max_height
!= null and max_height
< -dy
+ line_height
then break
389 else if c
== pld
then
390 dy
-= partial_line_skip
393 else if c
== plu
then
394 dy
+= partial_line_skip
397 else if c
.is_whitespace
then
398 var space_advance
= if desc
.chars
.keys
.has
(' ') then
399 desc
.chars
[' '].xadvance
400 else if desc
.chars
.keys
.has
('f') then
401 desc
.chars
['f'].xadvance
409 # If we care about line width, check for overflow
410 if max_width
!= null then
411 # Calculate the length of the next word
414 for wi
in [i
+1..text
.length
[ do
417 if w
== '\n' or w
== pld
or w
== plu
or w
.is_whitespace
then break
418 word_len
+= advance
(prev_w
, w
)
422 # Would the line be too long?
423 if dx
+ word_len
> max_width
then
424 if text_sprites
.wrap
then
426 justify
(line_sprites
, text_sprites
.align
, dx
)
428 if max_height
!= null and max_height
< -dy
+ line_height
then break
432 justify
(line_sprites
, text_sprites
.align
, dx
)
434 if max_height
!= null and max_height
< -dy
+ line_height
then break
436 while c
!= '\n' and i
< text
.length
- 1 do
448 # Replace or skip unknown characters
449 if not desc
.chars
.keys
.has
(c
) then
450 var rc
= replacement_char
451 if rc
== null then continue
455 var char_info
= desc
.chars
[c
]
456 var advance
= char_info
.xadvance
457 var kerning
= desc
.kerning
(prev_char
, c
)
459 var x
= dx
+ char_info
.width
/2.0 + char_info
.xoffset
+ kerning
460 var y
= dy
- char_info
.height
/2.0 - char_info
.yoffset
461 var pos
= text_sprites
.anchor
.offset
(x
, y
, 0.0)
462 var s
= new Sprite(char_info
.subtexture
, pos
)
463 text_sprites
.sprites
.add s
466 dx
+= advance
+ kerning
469 text_width
= text_width
.max
(dx
)
472 justify
(line_sprites
, text_sprites
.align
, dx
)
475 if text_sprites
.valign
!= 0.0 then
476 var d
= (-dy
+line_height
) * text_sprites
.valign
477 for s
in text_sprites
.sprites
do s
.center
.y
+= d
480 text_sprites
.width
= text_width
.max
(dx
)
481 text_sprites
.height
= dy
+ line_height
484 # Character replacing other characters missing from the font
485 private var replacement_char
: nullable Char is lazy
do
486 for c
in "�?".chars
do
487 if desc
.chars
.keys
.has
(c
) then return c
492 private fun advance
(prev_char
: nullable Char, char
: Char): Float
494 var char_info
= desc
.chars
[char
]
495 var kerning
= desc
.kerning
(prev_char
, char
)
496 return char_info
.xadvance
+ kerning
499 private fun justify
(line_sprites
: Array[Sprite], align
: Float, line_width
: Float)
501 var dx
= -line_width
*align
502 for s
in line_sprites
do s
.center
.x
+= dx