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
: RootTexture
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
, writable
150 # Scale to apply to this char only
151 var scale
= 1.0 is writable
156 # Parse `self` as an XML BMFont description file
158 # Reports only basic XML format errors, other errors may be ignored or
164 # <info face="arial" size="72" bold="0" italic="0" charset=""
165 # unicode="1" stretchH="100" smooth="1" aa="1" padding="2,2,2,2"
166 # spacing="0,0" outline="0"/>
167 # <common lineHeight="80" base="65" scaleW="4030" scaleH="231"
168 # pages="1" packed="0"/>
170 # <page id="0" file="arial.png"/>
173 # <char id="65" x="2519" y="10" width="55" height="59" xoffset="0"
174 # yoffset="13" xadvance="48" page="0" chnl="15"/>
175 # <char id="66" x="2600" y="10" width="46" height="58" xoffset="5"
176 # yoffset="13" xadvance="48" page="0" chnl="15"/>
177 # <char id="67" x="2673" y="9" width="52" height="60" xoffset="4"
178 # yoffset="12" xadvance="52" page="0" chnl="15"/>
180 # <kernings count="1">
181 # <kerning first="65" second="67" amount="-1"/>
186 # var fnt = desc.parse_bmfont("dir_in_assets").value
187 # assert fnt.to_s == "<BMFont arial at 72.0 pt, 1 pages, 3 chars>"
188 # assert fnt.line_height == 80.0
189 # assert fnt.kernings['A', 'C'] == -1.0
190 # assert fnt.chars['A'].page.as(TextureAsset).path == "dir_in_assets/arial.png"
192 fun parse_bmfont
(dir
: String): MaybeError[BMFont, Error]
196 if xml
isa XMLError then
197 var msg
= "XML Parse Error: {xml.message}:{xml.location or else 0}"
198 return new MaybeError[BMFont, Error](maybe_error
=new Error(msg
))
202 var roots
= xml
["font"]
203 if roots
.is_empty
then
204 var msg
= "Error: the XML document doesn't declare the expected `font` root"
205 return new MaybeError[BMFont, Error](maybe_error
=new Error(msg
))
208 # Expect the rest of the document to be well formatted
209 var root
= roots
.first
211 var info
= root
["info"].first
212 assert info
isa XMLAttrTag
213 var info_map
= info
.attributes_to_map
215 var common
= root
["common"].first
216 assert common
isa XMLAttrTag
217 var common_map
= common
.attributes_to_map
219 var fnt
= new BMFont(
221 info_map
["size"].to_f
,
222 info_map
["bold"] == "1",
223 info_map
["italic"] == "1",
224 info_map
["unicode"] == "1",
227 common_map
["lineHeight"].to_f
,
228 common_map
["base"].to_f
,
229 common_map
["scaleW"].to_f
,
230 common_map
["scaleH"].to_f
233 # Pages / pixel data files
234 var xml_pages
= root
["pages"].first
235 for page
in xml_pages
["page"] do
236 if not page
isa XMLAttrTag then continue
238 var attributes
= page
.attributes_to_map
239 var file
= dir
/ attributes
["file"]
240 fnt
.pages
[attributes
["id"]] = new TextureAsset(file
)
244 for item
in root
["chars"].first
["char"] do
245 if not item
isa XMLAttrTag then continue
247 var attributes
= item
.attributes_to_map
248 var id
= attributes
["id"].to_i
.code_point
250 var c
= new BMFontChar(
251 attributes
["x"].to_f
, attributes
["y"].to_f
,
252 attributes
["width"].to_f
, attributes
["height"].to_f
,
253 attributes
["xoffset"].to_f
, attributes
["yoffset"].to_f
,
254 attributes
["xadvance"].to_f
,
255 fnt
.pages
[attributes
["page"]])
260 # Kerning between two characters
261 var kernings
= root
["kernings"]
262 if kernings
.not_empty
then
263 for item
in kernings
.first
["kerning"] do
264 if not item
isa XMLAttrTag then continue
266 var attributes
= item
.attributes_to_map
267 var first
= attributes
["first"].to_i
.code_point
268 var second
= attributes
["second"].to_i
.code_point
269 var amount
= attributes
["amount"].to_f
270 fnt
.kernings
[first
, second
] = amount
274 return new MaybeError[BMFont, Error](fnt
)
278 # BMFont from the assets folder
282 # var font = new BMFontAsset("arial.fnt")
283 # var pos: Point3d[Float] = ui_camera.top_left.offset(128.0, -128.0, 0.0)
284 # var ui_text = new TextSprites(font, pos)
286 # redef fun create_scene
291 # assert font.error == null
293 # ui_text.text = "Hello world!"
303 # Require: `error == null`
307 var cache
= desc_cache
308 if cache
!= null then return cache
310 assert error
== null else print_error error
312 # Load on first access
315 assert error
== null else print_error error
317 return desc_cache
.as(not null)
320 private var desc_cache
: nullable BMFont = null
323 var error
: nullable Error = null
325 # XML description in the assets folder
326 private var text_asset
= new TextAsset(path
) is lateinit
328 # Load font description and textures from the assets folder
330 # Sets `error` if an error occurred, otherwise
331 # the font description can be accessed via `desc`.
334 var text_asset
= text_asset
336 var error
= text_asset
.error
337 if error
!= null then
342 var desc
= text_asset
.to_s
343 var fnt_or_error
= desc
.parse_bmfont
(path
.dirname
)
344 if fnt_or_error
.is_error
then
345 self.error
= fnt_or_error
.error
349 var fnt
= fnt_or_error
.value
350 self.desc_cache
= fnt
353 for page_name
, texture
in fnt
.pages
do
356 # Move up any texture loading error.
357 # This algo keeps only the latest error,
358 # but this isn't a problem on single page fonts.
359 error
= texture
.error
360 if error
!= null then self.error
= error
364 redef fun write_into
(text_sprites
, text
)
369 var line_sprites
= new Array[Sprite]
372 # Has the current line height been added to `height`?
373 var line_height_counted
= false
375 # TextSprite customization
376 var max_width
= text_sprites
.max_width
377 var max_height
= text_sprites
.max_height
378 var scale
= text_sprites
.scale
381 var line_height
= desc
.line_height
* scale
382 var partial_line_skip
= line_height
* partial_line_mod
.to_f
385 text_sprites
.links
.clear
387 var link_sprites
= new Array[Sprite]
390 # Loop over all characters
393 while i
< text
.length
- 1 do
398 var word_break
= false
400 justify
(line_sprites
, text_sprites
.align
, dx
)
402 if max_height
!= null and max_height
< -dy
+ line_height
then break
404 if not line_height_counted
then
405 # Force to account for empty lines
406 height
+= line_height
408 line_height_counted
= false
411 else if c
== pld
then
412 dy
-= partial_line_skip
413 height
+= partial_line_skip
416 else if c
== plu
then
417 dy
+= partial_line_skip
418 height
-= partial_line_skip
# We could keep two heights and return the max
421 else if c
.is_whitespace
then
422 var space_advance
= if desc
.chars
.keys
.has
(' ') then
423 desc
.chars
[' '].xadvance
424 else if desc
.chars
.keys
.has
('f') then
425 desc
.chars
['f'].xadvance
427 dx
+= space_advance
* scale
429 else if c
== '[' then
431 if i
+ 1 < text
.length
and text
[i
+1] == '[' then
432 # Escape if duplicated
438 else if c
== ']' then
440 if i
+ 1 < text
.length
and text
[i
+1] == ']' then
441 # Escape if duplicated
444 # If there's a () use it as link_name
446 if j
< text
.length
and text
[j
] == '(' then
451 if j
> text
.length
then
452 # No closing ), abort
458 if l
== ')' then break
461 if new_name
!= null then
467 # Register the link for the clients
468 text_sprites
.links
[link_name
] = link_sprites
472 link_sprites
= new Array[Sprite]
478 if in_link
then link_name
+= c
.to_s
482 # If we care about line width, check for overflow
483 if max_width
!= null then
484 # Calculate the length of the next word
487 for wi
in [i
+1..text
.length
[ do
490 if w
== '\n' or w
== pld
or w
== plu
or w
.is_whitespace
or (in_link
and w
== ']') then break
492 if not desc
.chars
.keys
.has
(w
) then
493 var rc
= replacement_char
494 if rc
== null then continue
498 word_len
+= advance
(prev_w
, w
) * scale
502 # Would the line be too long?
503 if dx
+ word_len
> max_width
then
504 justify
(line_sprites
, text_sprites
.align
, dx
)
506 if max_height
!= null and max_height
< -dy
+ line_height
then break
508 line_height_counted
= false
510 if not text_sprites
.wrap
then
511 # Cut short, skip everything until the next new line
512 while c
!= '\n' and i
< text
.length
- 1 do
524 # Replace or skip unknown characters
525 if not desc
.chars
.keys
.has
(c
) then
526 var rc
= replacement_char
527 if rc
== null then continue
531 var char_info
= desc
.chars
[c
]
532 var advance
= char_info
.xadvance
533 var kerning
= desc
.kerning
(prev_char
, c
)
535 var x
= dx
+ (char_info
.width
/2.0 + char_info
.xoffset
+ kerning
) * scale
536 var y
= dy
- (char_info
.height
/2.0 + char_info
.yoffset
) * scale
537 var pos
= text_sprites
.anchor
.offset
(x
, y
, 0.0)
538 var s
= new Sprite(char_info
.subtexture
, pos
)
539 s
.scale
= scale
* char_info
.scale
540 text_sprites
.sprites
.add s
542 if in_link
then link_sprites
.add s
544 dx
+= (advance
+ kerning
) * scale
547 text_width
= text_width
.max
(dx
)
549 if not line_height_counted
then
550 # Increase `height` only once per line iff there's a caracter
551 line_height_counted
= true
552 height
+= line_height
556 justify
(line_sprites
, text_sprites
.align
, dx
)
559 if text_sprites
.valign
!= 0.0 then
560 var d
= (-dy
+line_height
) * text_sprites
.valign
561 for s
in text_sprites
.sprites
do s
.center
.y
+= d
564 text_sprites
.width
= text_width
.max
(dx
)
565 text_sprites
.height
= height
568 # Character replacing other characters missing from the font
569 private var replacement_char
: nullable Char is lazy
do
570 for c
in "�?".chars
do
571 if desc
.chars
.keys
.has
(c
) then return c
576 private fun advance
(prev_char
: nullable Char, char
: Char): Float
578 var char_info
= desc
.chars
[char
]
579 var kerning
= desc
.kerning
(prev_char
, char
)
580 return char_info
.xadvance
+ kerning
583 private fun justify
(line_sprites
: Array[Sprite], align
: Float, line_width
: Float)
585 var dx
= -line_width
*align
586 for s
in line_sprites
do s
.center
.x
+= dx