Source: extras/BitmapText.js

extras/BitmapText.js

  1. import * as core from '../core';
  2. import ObservablePoint from '../core/math/ObservablePoint';
  3. import { getResolutionOfUrl } from '../core/utils';
  4. import settings from '../core/settings';
  5. /**
  6. * A BitmapText object will create a line or multiple lines of text using bitmap font. To
  7. * split a line you can use '\n', '\r' or '\r\n' in your string. You can generate the fnt files using:
  8. *
  9. * A BitmapText can only be created when the font is loaded
  10. *
  11. * ```js
  12. * // in this case the font is in a file called 'desyrel.fnt'
  13. * let bitmapText = new PIXI.extras.BitmapText("text using a fancy font!", {font: "35px Desyrel", align: "right"});
  14. * ```
  15. *
  16. * http://www.angelcode.com/products/bmfont/ for windows or
  17. * http://www.bmglyph.com/ for mac.
  18. *
  19. * @class
  20. * @extends PIXI.Container
  21. * @memberof PIXI.extras
  22. */
  23. export default class BitmapText extends core.Container
  24. {
  25. /**
  26. * @param {string} text - The copy that you would like the text to display
  27. * @param {object} style - The style parameters
  28. * @param {string|object} style.font - The font descriptor for the object, can be passed as a string of form
  29. * "24px FontName" or "FontName" or as an object with explicit name/size properties.
  30. * @param {string} [style.font.name] - The bitmap font id
  31. * @param {number} [style.font.size] - The size of the font in pixels, e.g. 24
  32. * @param {string} [style.align='left'] - Alignment for multiline text ('left', 'center' or 'right'), does not affect
  33. * single line text
  34. * @param {number} [style.tint=0xFFFFFF] - The tint color
  35. */
  36. constructor(text, style = {})
  37. {
  38. super();
  39. /**
  40. * Private tracker for the width of the overall text
  41. *
  42. * @member {number}
  43. * @private
  44. */
  45. this._textWidth = 0;
  46. /**
  47. * Private tracker for the height of the overall text
  48. *
  49. * @member {number}
  50. * @private
  51. */
  52. this._textHeight = 0;
  53. /**
  54. * Private tracker for the letter sprite pool.
  55. *
  56. * @member {PIXI.Sprite[]}
  57. * @private
  58. */
  59. this._glyphs = [];
  60. /**
  61. * Private tracker for the current style.
  62. *
  63. * @member {object}
  64. * @private
  65. */
  66. this._font = {
  67. tint: style.tint !== undefined ? style.tint : 0xFFFFFF,
  68. align: style.align || 'left',
  69. name: null,
  70. size: 0,
  71. };
  72. /**
  73. * Private tracker for the current font.
  74. *
  75. * @member {object}
  76. * @private
  77. */
  78. this.font = style.font; // run font setter
  79. /**
  80. * Private tracker for the current text.
  81. *
  82. * @member {string}
  83. * @private
  84. */
  85. this._text = text;
  86. /**
  87. * The max width of this bitmap text in pixels. If the text provided is longer than the
  88. * value provided, line breaks will be automatically inserted in the last whitespace.
  89. * Disable by setting value to 0
  90. *
  91. * @member {number}
  92. * @private
  93. */
  94. this._maxWidth = 0;
  95. /**
  96. * The max line height. This is useful when trying to use the total height of the Text,
  97. * ie: when trying to vertically align.
  98. *
  99. * @member {number}
  100. * @private
  101. */
  102. this._maxLineHeight = 0;
  103. /**
  104. * Letter spacing. This is useful for setting the space between characters.
  105. * @member {number}
  106. * @private
  107. */
  108. this._letterSpacing = 0;
  109. /**
  110. * Text anchor. read-only
  111. *
  112. * @member {PIXI.ObservablePoint}
  113. * @private
  114. */
  115. this._anchor = new ObservablePoint(() => { this.dirty = true; }, this, 0, 0);
  116. /**
  117. * The dirty state of this object.
  118. *
  119. * @member {boolean}
  120. */
  121. this.dirty = false;
  122. this.updateText();
  123. }
  124. /**
  125. * Renders text and updates it when needed
  126. *
  127. * @private
  128. */
  129. updateText()
  130. {
  131. const data = BitmapText.fonts[this._font.name];
  132. const scale = this._font.size / data.size;
  133. const pos = new core.Point();
  134. const chars = [];
  135. const lineWidths = [];
  136. const text = this.text.replace(/(?:\r\n|\r)/g, '\n');
  137. const textLength = text.length;
  138. const maxWidth = this._maxWidth * data.size / this._font.size;
  139. let prevCharCode = null;
  140. let lastLineWidth = 0;
  141. let maxLineWidth = 0;
  142. let line = 0;
  143. let lastBreakPos = -1;
  144. let lastBreakWidth = 0;
  145. let spacesRemoved = 0;
  146. let maxLineHeight = 0;
  147. for (let i = 0; i < textLength; i++)
  148. {
  149. const charCode = text.charCodeAt(i);
  150. const char = text.charAt(i);
  151. if (/(?:\s)/.test(char))
  152. {
  153. lastBreakPos = i;
  154. lastBreakWidth = lastLineWidth;
  155. }
  156. if (char === '\r' || char === '\n')
  157. {
  158. lineWidths.push(lastLineWidth);
  159. maxLineWidth = Math.max(maxLineWidth, lastLineWidth);
  160. ++line;
  161. ++spacesRemoved;
  162. pos.x = 0;
  163. pos.y += data.lineHeight;
  164. prevCharCode = null;
  165. continue;
  166. }
  167. const charData = data.chars[charCode];
  168. if (!charData)
  169. {
  170. continue;
  171. }
  172. if (prevCharCode && charData.kerning[prevCharCode])
  173. {
  174. pos.x += charData.kerning[prevCharCode];
  175. }
  176. chars.push({
  177. texture: charData.texture,
  178. line,
  179. charCode,
  180. position: new core.Point(pos.x + charData.xOffset + (this._letterSpacing / 2), pos.y + charData.yOffset),
  181. });
  182. pos.x += charData.xAdvance + this._letterSpacing;
  183. lastLineWidth = pos.x;
  184. maxLineHeight = Math.max(maxLineHeight, (charData.yOffset + charData.texture.height));
  185. prevCharCode = charCode;
  186. if (lastBreakPos !== -1 && maxWidth > 0 && pos.x > maxWidth)
  187. {
  188. ++spacesRemoved;
  189. core.utils.removeItems(chars, 1 + lastBreakPos - spacesRemoved, 1 + i - lastBreakPos);
  190. i = lastBreakPos;
  191. lastBreakPos = -1;
  192. lineWidths.push(lastBreakWidth);
  193. maxLineWidth = Math.max(maxLineWidth, lastBreakWidth);
  194. line++;
  195. pos.x = 0;
  196. pos.y += data.lineHeight;
  197. prevCharCode = null;
  198. }
  199. }
  200. const lastChar = text.charAt(text.length - 1);
  201. if (lastChar !== '\r' && lastChar !== '\n')
  202. {
  203. if (/(?:\s)/.test(lastChar))
  204. {
  205. lastLineWidth = lastBreakWidth;
  206. }
  207. lineWidths.push(lastLineWidth);
  208. maxLineWidth = Math.max(maxLineWidth, lastLineWidth);
  209. }
  210. const lineAlignOffsets = [];
  211. for (let i = 0; i <= line; i++)
  212. {
  213. let alignOffset = 0;
  214. if (this._font.align === 'right')
  215. {
  216. alignOffset = maxLineWidth - lineWidths[i];
  217. }
  218. else if (this._font.align === 'center')
  219. {
  220. alignOffset = (maxLineWidth - lineWidths[i]) / 2;
  221. }
  222. lineAlignOffsets.push(alignOffset);
  223. }
  224. const lenChars = chars.length;
  225. const tint = this.tint;
  226. for (let i = 0; i < lenChars; i++)
  227. {
  228. let c = this._glyphs[i]; // get the next glyph sprite
  229. if (c)
  230. {
  231. c.texture = chars[i].texture;
  232. }
  233. else
  234. {
  235. c = new core.Sprite(chars[i].texture);
  236. this._glyphs.push(c);
  237. }
  238. c.position.x = (chars[i].position.x + lineAlignOffsets[chars[i].line]) * scale;
  239. c.position.y = chars[i].position.y * scale;
  240. c.scale.x = c.scale.y = scale;
  241. c.tint = tint;
  242. if (!c.parent)
  243. {
  244. this.addChild(c);
  245. }
  246. }
  247. // remove unnecessary children.
  248. for (let i = lenChars; i < this._glyphs.length; ++i)
  249. {
  250. this.removeChild(this._glyphs[i]);
  251. }
  252. this._textWidth = maxLineWidth * scale;
  253. this._textHeight = (pos.y + data.lineHeight) * scale;
  254. // apply anchor
  255. if (this.anchor.x !== 0 || this.anchor.y !== 0)
  256. {
  257. for (let i = 0; i < lenChars; i++)
  258. {
  259. this._glyphs[i].x -= this._textWidth * this.anchor.x;
  260. this._glyphs[i].y -= this._textHeight * this.anchor.y;
  261. }
  262. }
  263. this._maxLineHeight = maxLineHeight * scale;
  264. }
  265. /**
  266. * Updates the transform of this object
  267. *
  268. * @private
  269. */
  270. updateTransform()
  271. {
  272. this.validate();
  273. this.containerUpdateTransform();
  274. }
  275. /**
  276. * Validates text before calling parent's getLocalBounds
  277. *
  278. * @return {PIXI.Rectangle} The rectangular bounding area
  279. */
  280. getLocalBounds()
  281. {
  282. this.validate();
  283. return super.getLocalBounds();
  284. }
  285. /**
  286. * Updates text when needed
  287. *
  288. * @private
  289. */
  290. validate()
  291. {
  292. if (this.dirty)
  293. {
  294. this.updateText();
  295. this.dirty = false;
  296. }
  297. }
  298. /**
  299. * The tint of the BitmapText object
  300. *
  301. * @member {number}
  302. */
  303. get tint()
  304. {
  305. return this._font.tint;
  306. }
  307. set tint(value) // eslint-disable-line require-jsdoc
  308. {
  309. this._font.tint = (typeof value === 'number' && value >= 0) ? value : 0xFFFFFF;
  310. this.dirty = true;
  311. }
  312. /**
  313. * The alignment of the BitmapText object
  314. *
  315. * @member {string}
  316. * @default 'left'
  317. */
  318. get align()
  319. {
  320. return this._font.align;
  321. }
  322. set align(value) // eslint-disable-line require-jsdoc
  323. {
  324. this._font.align = value || 'left';
  325. this.dirty = true;
  326. }
  327. /**
  328. * The anchor sets the origin point of the text.
  329. * The default is 0,0 this means the text's origin is the top left
  330. * Setting the anchor to 0.5,0.5 means the text's origin is centered
  331. * Setting the anchor to 1,1 would mean the text's origin point will be the bottom right corner
  332. *
  333. * @member {PIXI.Point | number}
  334. */
  335. get anchor()
  336. {
  337. return this._anchor;
  338. }
  339. set anchor(value) // eslint-disable-line require-jsdoc
  340. {
  341. if (typeof value === 'number')
  342. {
  343. this._anchor.set(value);
  344. }
  345. else
  346. {
  347. this._anchor.copy(value);
  348. }
  349. }
  350. /**
  351. * The font descriptor of the BitmapText object
  352. *
  353. * @member {string|object}
  354. */
  355. get font()
  356. {
  357. return this._font;
  358. }
  359. set font(value) // eslint-disable-line require-jsdoc
  360. {
  361. if (!value)
  362. {
  363. return;
  364. }
  365. if (typeof value === 'string')
  366. {
  367. value = value.split(' ');
  368. this._font.name = value.length === 1 ? value[0] : value.slice(1).join(' ');
  369. this._font.size = value.length >= 2 ? parseInt(value[0], 10) : BitmapText.fonts[this._font.name].size;
  370. }
  371. else
  372. {
  373. this._font.name = value.name;
  374. this._font.size = typeof value.size === 'number' ? value.size : parseInt(value.size, 10);
  375. }
  376. this.dirty = true;
  377. }
  378. /**
  379. * The text of the BitmapText object
  380. *
  381. * @member {string}
  382. */
  383. get text()
  384. {
  385. return this._text;
  386. }
  387. set text(value) // eslint-disable-line require-jsdoc
  388. {
  389. value = value.toString() || ' ';
  390. if (this._text === value)
  391. {
  392. return;
  393. }
  394. this._text = value;
  395. this.dirty = true;
  396. }
  397. /**
  398. * The max width of this bitmap text in pixels. If the text provided is longer than the
  399. * value provided, line breaks will be automatically inserted in the last whitespace.
  400. * Disable by setting value to 0
  401. *
  402. * @member {number}
  403. */
  404. get maxWidth()
  405. {
  406. return this._maxWidth;
  407. }
  408. set maxWidth(value) // eslint-disable-line require-jsdoc
  409. {
  410. if (this._maxWidth === value)
  411. {
  412. return;
  413. }
  414. this._maxWidth = value;
  415. this.dirty = true;
  416. }
  417. /**
  418. * The max line height. This is useful when trying to use the total height of the Text,
  419. * ie: when trying to vertically align.
  420. *
  421. * @member {number}
  422. * @readonly
  423. */
  424. get maxLineHeight()
  425. {
  426. this.validate();
  427. return this._maxLineHeight;
  428. }
  429. /**
  430. * The width of the overall text, different from fontSize,
  431. * which is defined in the style object
  432. *
  433. * @member {number}
  434. * @readonly
  435. */
  436. get textWidth()
  437. {
  438. this.validate();
  439. return this._textWidth;
  440. }
  441. /**
  442. * Additional space between characters.
  443. *
  444. * @member {number}
  445. */
  446. get letterSpacing()
  447. {
  448. return this._letterSpacing;
  449. }
  450. set letterSpacing(value) // eslint-disable-line require-jsdoc
  451. {
  452. if (this._letterSpacing !== value)
  453. {
  454. this._letterSpacing = value;
  455. this.dirty = true;
  456. }
  457. }
  458. /**
  459. * The height of the overall text, different from fontSize,
  460. * which is defined in the style object
  461. *
  462. * @member {number}
  463. * @readonly
  464. */
  465. get textHeight()
  466. {
  467. this.validate();
  468. return this._textHeight;
  469. }
  470. /**
  471. * Register a bitmap font with data and a texture.
  472. *
  473. * @static
  474. * @param {XMLDocument} xml - The XML document data.
  475. * @param {Object.<string, PIXI.Texture>|PIXI.Texture|PIXI.Texture[]} textures - List of textures for each page.
  476. * If providing an object, the key is the `<page>` element's `file` attribute in the FNT file.
  477. * @return {Object} Result font object with font, size, lineHeight and char fields.
  478. */
  479. static registerFont(xml, textures)
  480. {
  481. const data = {};
  482. const info = xml.getElementsByTagName('info')[0];
  483. const common = xml.getElementsByTagName('common')[0];
  484. const pages = xml.getElementsByTagName('page');
  485. const res = getResolutionOfUrl(pages[0].getAttribute('file'), settings.RESOLUTION);
  486. const pagesTextures = {};
  487. data.font = info.getAttribute('face');
  488. data.size = parseInt(info.getAttribute('size'), 10);
  489. data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10) / res;
  490. data.chars = {};
  491. // Single texture, convert to list
  492. if (textures instanceof core.Texture)
  493. {
  494. textures = [textures];
  495. }
  496. // Convert the input Texture, Textures or object
  497. // into a page Texture lookup by "id"
  498. for (let i = 0; i < pages.length; i++)
  499. {
  500. const id = pages[i].getAttribute('id');
  501. const file = pages[i].getAttribute('file');
  502. pagesTextures[id] = textures instanceof Array ? textures[i] : textures[file];
  503. }
  504. // parse letters
  505. const letters = xml.getElementsByTagName('char');
  506. for (let i = 0; i < letters.length; i++)
  507. {
  508. const letter = letters[i];
  509. const charCode = parseInt(letter.getAttribute('id'), 10);
  510. const page = letter.getAttribute('page') || 0;
  511. const textureRect = new core.Rectangle(
  512. (parseInt(letter.getAttribute('x'), 10) / res) + (pagesTextures[page].frame.x / res),
  513. (parseInt(letter.getAttribute('y'), 10) / res) + (pagesTextures[page].frame.y / res),
  514. parseInt(letter.getAttribute('width'), 10) / res,
  515. parseInt(letter.getAttribute('height'), 10) / res
  516. );
  517. data.chars[charCode] = {
  518. xOffset: parseInt(letter.getAttribute('xoffset'), 10) / res,
  519. yOffset: parseInt(letter.getAttribute('yoffset'), 10) / res,
  520. xAdvance: parseInt(letter.getAttribute('xadvance'), 10) / res,
  521. kerning: {},
  522. texture: new core.Texture(pagesTextures[page].baseTexture, textureRect),
  523. page,
  524. };
  525. }
  526. // parse kernings
  527. const kernings = xml.getElementsByTagName('kerning');
  528. for (let i = 0; i < kernings.length; i++)
  529. {
  530. const kerning = kernings[i];
  531. const first = parseInt(kerning.getAttribute('first'), 10) / res;
  532. const second = parseInt(kerning.getAttribute('second'), 10) / res;
  533. const amount = parseInt(kerning.getAttribute('amount'), 10) / res;
  534. if (data.chars[second])
  535. {
  536. data.chars[second].kerning[first] = amount;
  537. }
  538. }
  539. // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3
  540. // but it's very likely to change
  541. BitmapText.fonts[data.font] = data;
  542. return data;
  543. }
  544. }
  545. BitmapText.fonts = {};