8ad2dce313dc29ca7f09ff58277e6125055b41c0
[nit.git] / lib / gamnit / bmfont.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15 # Parse Angel Code BMFont format and draw text
16 #
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/
21 #
22 # Format reference: http://www.angelcode.com/products/bmfont/doc/file_format.html
23 module bmfont
24
25 private import dom
26
27 import font
28
29 # BMFont description, parsed with `Text::parse_bmfont` or loaded as a `BMFontAsset`
30 #
31 # This class flattens all the `info` and `common` data.
32 class BMFont
33
34 # ---
35 # info part
36 #
37 # How the font was generated.
38
39 # Name of the source true type font
40 var face: Text
41
42 # Size of the source true type font
43 var size: Float
44
45 # Is the font bold?
46 var bold: Bool
47
48 # Is the font italic?
49 var italic: Bool
50
51 # Does the font uses the Unicode charset?
52 var unicode: Bool
53
54 # Padding for each character
55 #
56 # In the format `up,right,down,left`
57 var padding: String
58
59 # Spacing for each character
60 #
61 # In the format `horizontal,vertical`.
62 var spacing: String
63
64 # ---
65 # common part
66 #
67 # Information common to all characters
68
69 # Distance in pixels between each line of text
70 var line_height: Float
71
72 # Pixels from the top of the line to the base of the characters
73 var base: Float
74
75 # Width of the texture
76 var scale_w: Float
77
78 # Height of the texture
79 var scale_h: Float
80
81 # Textures
82 var pages = new Map[String, TextureAsset]
83
84 # Characters in the font
85 var chars = new Map[Char, BMFontChar]
86
87 # Distance between certain characters
88 var kernings = new HashMap2[Char, Char, Float]
89
90 redef fun to_s do return "<{class_name} {face} at {size} pt, "+
91 "{pages.length} pages, {chars.length} chars>"
92
93 # TODO
94 #
95 # # From info
96 # charset
97 # stretchH
98 # smooth
99 # aa
100 # outline
101 #
102 # # From common
103 # packed
104 # alphaChnl
105 # redChnl
106 # greenChnl
107 # blueChnl
108 end
109
110 # Description of a character in a `BMFont`
111 class BMFontChar
112
113 # Subtexture left coordinate
114 var x: Float
115
116 # Subtexture top coordinate
117 var y: Float
118
119 # Subtexture width
120 var width: Float
121
122 # Subtexture height
123 var height: Float
124
125 # Drawing offset on X
126 var xoffset: Float
127
128 # Drawing offset on Y
129 var yoffset: Float
130
131 # Cursor advance after drawing this character
132 var xadvance: Float
133
134 # Full texture contaning this character and others
135 var page: TextureAsset
136
137 # TODO Channel where the image is found
138 #var chnl: Int
139
140 # Subtexture with this character image only
141 var subtexture: Texture = page.subtexture(x, y, width, height) is lazy
142 end
143
144 redef class Text
145
146 # Parse `self` as an XML BMFont description file
147 #
148 # Reports only basic XML format errors, other errors may be ignored or
149 # cause a crash.
150 #
151 # ~~~
152 # var desc = """
153 # <font>
154 # <info face="arial" size="72" bold="0" italic="0" charset=""
155 # unicode="1" stretchH="100" smooth="1" aa="1" padding="2,2,2,2"
156 # spacing="0,0" outline="0"/>
157 # <common lineHeight="80" base="65" scaleW="4030" scaleH="231"
158 # pages="1" packed="0"/>
159 # <pages>
160 # <page id="0" file="arial.png"/>
161 # </pages>
162 # <chars count="3">
163 # <char id="65" x="2519" y="10" width="55" height="59" xoffset="0"
164 # yoffset="13" xadvance="48" page="0" chnl="15"/>
165 # <char id="66" x="2600" y="10" width="46" height="58" xoffset="5"
166 # yoffset="13" xadvance="48" page="0" chnl="15"/>
167 # <char id="67" x="2673" y="9" width="52" height="60" xoffset="4"
168 # yoffset="12" xadvance="52" page="0" chnl="15"/>
169 # </chars>
170 # <kernings count="1">
171 # <kerning first="65" second="67" amount="-1"/>
172 # </kernings>
173 # </font>
174 # """
175 #
176 # var fnt = desc.parse_bmfont("dir_in_assets").value
177 # assert fnt.to_s == "<BMFont arial at 72.0 pt, 1 pages, 3 chars>"
178 # assert fnt.line_height == 80.0
179 # assert fnt.kernings['A', 'C'] == -1.0
180 # assert fnt.chars['A'].page.path == "dir_in_assets/arial.png"
181 # ~~~
182 fun parse_bmfont(dir: String): MaybeError[BMFont, Error]
183 do
184 # Parse XML
185 var xml = to_xml
186 if xml isa XMLError then
187 var msg = "XML Parse Error: {xml.message}:{xml.location or else 0}"
188 return new MaybeError[BMFont, Error](maybe_error=new Error(msg))
189 end
190
191 # Basic sanity check
192 var roots = xml["font"]
193 if roots.is_empty then
194 var msg = "Error: the XML document doesn't declare the expected `font` root"
195 return new MaybeError[BMFont, Error](maybe_error=new Error(msg))
196 end
197
198 # Expect the rest of the document to be well formatted
199 var root = roots.first
200
201 var info = root["info"].first
202 assert info isa XMLAttrTag
203 var info_map = info.attributes_to_map
204
205 var common = root["common"].first
206 assert common isa XMLAttrTag
207 var common_map = common.attributes_to_map
208
209 var fnt = new BMFont(
210 info_map["face"],
211 info_map["size"].to_f,
212 info_map["bold"] == "1",
213 info_map["italic"] == "1",
214 info_map["unicode"] == "1",
215 info_map["padding"],
216 info_map["spacing"],
217 common_map["lineHeight"].to_f,
218 common_map["base"].to_f,
219 common_map["scaleW"].to_f,
220 common_map["scaleH"].to_f
221 )
222
223 # Pages / pixel data files
224 var xml_pages = root["pages"].first
225 for page in xml_pages["page"] do
226 if not page isa XMLAttrTag then continue
227
228 var attributes = page.attributes_to_map
229 var file = dir / attributes["file"]
230 fnt.pages[attributes["id"]] = new TextureAsset(file)
231 end
232
233 # Char description
234 for item in root["chars"].first["char"] do
235 if not item isa XMLAttrTag then continue
236
237 var attributes = item.attributes_to_map
238 var id = attributes["id"].to_i.code_point
239
240 var c = new BMFontChar(
241 attributes["x"].to_f, attributes["y"].to_f,
242 attributes["width"].to_f, attributes["height"].to_f,
243 attributes["xoffset"].to_f, attributes["yoffset"].to_f,
244 attributes["xadvance"].to_f,
245 fnt.pages[attributes["page"]])
246
247 fnt.chars[id] = c
248 end
249
250 # Kerning between two characters
251 var kernings = root["kernings"]
252 if kernings.not_empty then
253 for item in kernings.first["kerning"] do
254 if not item isa XMLAttrTag then continue
255
256 var attributes = item.attributes_to_map
257 var first = attributes["first"].to_i.code_point
258 var second = attributes["second"].to_i.code_point
259 var amount = attributes["amount"].to_f
260 fnt.kernings[first, second] = amount
261 end
262 end
263
264 return new MaybeError[BMFont, Error](fnt)
265 end
266 end
267
268 # BMFont from the assets folder
269 #
270 # ~~~
271 # redef class App
272 # var font = new BMFontAsset("arial.fnt")
273 # var ui_text = new TextSprites(font, ui_camera.top_left)
274 #
275 # redef fun on_create
276 # do
277 # super
278 #
279 # font.load
280 # assert font.error == null
281 #
282 # ui_text.text = "Hello world!"
283 # end
284 # end
285 # ~~~
286 class BMFontAsset
287 super Asset
288 super Font
289
290 # Font description
291 #
292 # Require: `error == null`
293 fun desc: BMFont
294 do
295 # Cached results
296 var cache = desc_cache
297 if cache != null then return cache
298 var error = error
299 assert error == null else print_error error
300
301 # Load on first access
302 load
303 error = self.error
304 assert error == null else print_error error
305
306 return desc_cache.as(not null)
307 end
308
309 private var desc_cache: nullable BMFont = null
310
311 # Error at loading
312 var error: nullable Error = null
313
314 # XML description in the assets folder
315 private var text_asset = new TextAsset(path) is lateinit
316
317 # Load font description and textures from the assets folder
318 #
319 # Sets `error` if an error occurred, otherwise
320 # the font description can be accessed via `desc`.
321 fun load
322 do
323 var text_asset = text_asset
324 text_asset.load
325 var error = text_asset.error
326 if error != null then
327 self.error = error
328 return
329 end
330
331 var desc = text_asset.to_s
332 var fnt_or_error = desc.parse_bmfont(path.dirname)
333 if fnt_or_error.is_error then
334 self.error = fnt_or_error.error
335 return
336 end
337
338 var fnt = fnt_or_error.value
339 self.desc_cache = fnt
340
341 # Load textures too
342 for page_name, texture in fnt.pages do
343 texture.load
344
345 # Move up any texture loading error.
346 # This algo keeps only the latest error,
347 # but this isn't a problem on single page fonts.
348 error = texture.error
349 if error != null then self.error = error
350 end
351 end
352
353 redef fun write_into(text_sprites, text)
354 do
355 var dx = 0.0
356 var dy = 0.0
357
358 var line_height = desc.line_height
359 var partial_line_skip = line_height * partial_line_mod.to_f
360
361 var prev_char = null
362 for c in text do
363
364 # Special characters
365 if c == '\n' then
366 dy -= line_height.to_f
367 dx = 0.0 #advance/2.0
368 prev_char = null
369 continue
370 else if c == pld then
371 dy -= partial_line_skip
372 prev_char = null
373 continue
374 else if c == plu then
375 dy += partial_line_skip
376 prev_char = null
377 continue
378 else if c.is_whitespace then
379 var advance = if desc.chars.keys.has(' ') then
380 desc.chars[' '].xadvance
381 else if desc.chars.keys.has('f') then
382 desc.chars['f'].xadvance
383 else 16.0
384 dx += advance
385 prev_char = null
386 continue
387 end
388
389 # Replace or skip unknown characters
390 if not desc.chars.keys.has(c) then
391 var rc = replacement_char
392 if rc == null then continue
393 c = rc
394 end
395
396 var char_info = desc.chars[c]
397 var advance = char_info.xadvance
398
399 var kerning = 0.0
400 if prev_char != null then
401 kerning = desc.kernings[prev_char, c] or else 0.0
402 end
403
404 var x = dx + char_info.width/2.0 + char_info.xoffset + kerning
405 var y = dy - char_info.height/2.0 - char_info.yoffset
406 var pos = text_sprites.anchor.offset(x, y, 0.0)
407 text_sprites.sprites.add new Sprite(char_info.subtexture, pos)
408
409 dx += advance + kerning
410 prev_char = c
411 end
412 end
413
414 # Character replacing other charactesr missing from the font
415 private var replacement_char: nullable Char is lazy do
416 for c in "�?".chars do
417 if desc.chars.keys.has(c) then return c
418 end
419 return null
420 end
421 end