/** * @author Richard Davey <rich@photonstorm.com> * @copyright 2015 Photon Storm Ltd. * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} */ /** * BitmapText objects work by taking a texture file and an XML or JSON file that describes the font structure. * It then generates a new Sprite object for each letter of the text, proportionally spaced out and aligned to * match the font structure. * * BitmapText objects are less flexible than Text objects, in that they have less features such as shadows, fills and the ability * to use Web Fonts, however you trade this flexibility for rendering speed. You can also create visually compelling BitmapTexts by * processing the font texture in an image editor, applying fills and any other effects required. * * To create multi-line text insert \r, \n or \r\n escape codes into the text string. * * If you are having performance issues due to the volume of sprites being rendered, and do not require the text to be constantly * updating, you can use BitmapText.generateTexture to create a static texture from this BitmapText. * * To create a BitmapText data files you can use: * * BMFont (Windows, free): http://www.angelcode.com/products/bmfont/ * Glyph Designer (OS X, commercial): http://www.71squared.com/en/glyphdesigner * Littera (Web-based, free): http://kvazars.com/littera/ * * For most use cases it is recommended to use XML. If you wish to use JSON, the formatting should be equal to the result of * converting a valid XML file through the popular X2JS library. An online tool for conversion can be found here: http://codebeautify.org/xmltojson * * @class Phaser.BitmapText * @constructor * @extends PIXI.DisplayObjectContainer * @extends Phaser.Component.Core * @extends Phaser.Component.Angle * @extends Phaser.Component.AutoCull * @extends Phaser.Component.Bounds * @extends Phaser.Component.Destroy * @extends Phaser.Component.FixedToCamera * @extends Phaser.Component.InputEnabled * @extends Phaser.Component.InWorld * @extends Phaser.Component.LifeSpan * @extends Phaser.Component.PhysicsBody * @extends Phaser.Component.Reset * @param {Phaser.Game} game - A reference to the currently running game. * @param {number} x - X coordinate to display the BitmapText object at. * @param {number} y - Y coordinate to display the BitmapText object at. * @param {string} font - The key of the BitmapText as stored in Phaser.Cache. * @param {string} [text=''] - The text that will be rendered. This can also be set later via BitmapText.text. * @param {number} [size=32] - The size the font will be rendered at in pixels. * @param {string} [align='left'] - The alignment of multi-line text. Has no effect if there is only one line of text. */ Phaser.BitmapText = function (game, x, y, font, text, size, align) { x = x || 0; y = y || 0; font = font || ''; text = text || ''; size = size || 32; align = align || 'left'; PIXI.DisplayObjectContainer.call(this); /** * @property {number} type - The const type of this object. * @readonly */ this.type = Phaser.BITMAPTEXT; /** * @property {number} physicsType - The const physics body type of this object. * @readonly */ this.physicsType = Phaser.SPRITE; /** * @property {number} textWidth - The width in pixels of the overall text area, taking into consideration multi-line text. * @readOnly */ this.textWidth = 0; /** * @property {number} textHeight - The height in pixels of the overall text area, taking into consideration multi-line text. * @readOnly */ this.textHeight = 0; /** * @property {Phaser.Point} anchor - The anchor value of this BitmapText. */ this.anchor = new Phaser.Point(); /** * @property {Phaser.Point} _prevAnchor - The previous anchor value. * @private */ this._prevAnchor = new Phaser.Point(); /** * @property {array} _glyphs - Private tracker for the letter sprite pool. * @private */ this._glyphs = []; /** * @property {number} _maxWidth - Internal cache var. * @private */ this._maxWidth = 0; /** * @property {string} _text - Internal cache var. * @private */ this._text = text; /** * @property {string} _data - Internal cache var. * @private */ this._data = game.cache.getBitmapFont(font); /** * @property {string} _font - Internal cache var. * @private */ this._font = font; /** * @property {number} _fontSize - Internal cache var. * @private */ this._fontSize = size; /** * @property {string} _align - Internal cache var. * @private */ this._align = align; /** * @property {number} _tint - Internal cache var. * @private */ this._tint = 0xFFFFFF; this.updateText(); /** * @property {boolean} dirty - The dirty state of this object. */ this.dirty = false; Phaser.Component.Core.init.call(this, game, x, y, '', null); }; Phaser.BitmapText.prototype = Object.create(PIXI.DisplayObjectContainer.prototype); Phaser.BitmapText.prototype.constructor = Phaser.BitmapText; Phaser.Component.Core.install.call(Phaser.BitmapText.prototype, [ 'Angle', 'AutoCull', 'Bounds', 'Destroy', 'FixedToCamera', 'InputEnabled', 'InWorld', 'LifeSpan', 'PhysicsBody', 'Reset' ]); Phaser.BitmapText.prototype.preUpdatePhysics = Phaser.Component.PhysicsBody.preUpdate; Phaser.BitmapText.prototype.preUpdateLifeSpan = Phaser.Component.LifeSpan.preUpdate; Phaser.BitmapText.prototype.preUpdateInWorld = Phaser.Component.InWorld.preUpdate; Phaser.BitmapText.prototype.preUpdateCore = Phaser.Component.Core.preUpdate; /** * Automatically called by World.preUpdate. * * @method * @memberof Phaser.BitmapText * @return {boolean} True if the BitmapText was rendered, otherwise false. */ Phaser.BitmapText.prototype.preUpdate = function () { if (!this.preUpdatePhysics() || !this.preUpdateLifeSpan() || !this.preUpdateInWorld()) { return false; } return this.preUpdateCore(); }; /** * Automatically called by World.preUpdate. * @method Phaser.BitmapText.prototype.postUpdate */ Phaser.BitmapText.prototype.postUpdate = function () { Phaser.Component.PhysicsBody.postUpdate.call(this); Phaser.Component.FixedToCamera.postUpdate.call(this); if (this.body && this.body.type === Phaser.Physics.ARCADE) { if ((this.textWidth !== this.body.sourceWidth) || (this.textHeight !== this.body.sourceHeight)) { this.body.setSize(this.textWidth, this.textHeight); } } }; /** * The text to be displayed by this BitmapText object. * * It's faster to use `BitmapText.text = string`, but this is kept for backwards compatibility. * * @method Phaser.BitmapText.prototype.setText * @param {string} text - The text to be displayed by this BitmapText object. */ Phaser.BitmapText.prototype.setText = function (text) { this.text = text; }; /** * Given the input text this will scan the characters until either a newline is encountered, * or the line exceeds maxWidth, taking into account kerning, character widths and scaling. * * @method Phaser.BitmapText.prototype.scanLine * @private * @param {object} data - A reference to the font object in the Phaser.Cache. * @param {float} scale - The scale of the font in relation to the texture. * @param {string} text - The text to parse. * @return {object} An object containing the parsed characters, total pixel width and x offsets. */ Phaser.BitmapText.prototype.scanLine = function (data, scale, text) { var x = 0; var w = 0; var lastSpace = -1; var prevCharCode = null; var maxWidth = (this._maxWidth > 0) ? this._maxWidth : null; var chars = []; // Let's scan the text and work out if any of the lines are > maxWidth for (var i = 0; i < text.length; i++) { var end = (i === text.length - 1) ? true : false; if (/(?:\r\n|\r|\n)/.test(text.charAt(i))) { return { width: w, text: text.substr(0, i), end: end, chars: chars }; } else { var charCode = text.charCodeAt(i); var charData = data.chars[charCode]; var c = 0; if (!charData) { // Skip a character not found in the font data continue; } // Adjust for kerning from previous character to this one var kerning = (prevCharCode && charData.kerning[prevCharCode]) ? charData.kerning[prevCharCode] : 0; // Record the last space in the string lastSpace = /(\s)/.test(text.charAt(i)) ? i : lastSpace; // What will the line width be if we add this character to it? c = (kerning + charData.texture.width + charData.xOffset) * scale; // Do we need to line-wrap? if (maxWidth && ((w + c) >= maxWidth) && lastSpace > -1) { // The last space was at "lastSpace" which was "i - lastSpace" characters ago return { width: w, text: text.substr(0, i - (i - lastSpace)), end: end, chars: chars }; } else { w += charData.xAdvance * scale; chars.push(x + (charData.xOffset * scale)); x += charData.xAdvance * scale; prevCharCode = charCode; } } } return { width: w, text: text, end: end, chars: chars }; }; /** * Renders text and updates it when needed. * * @method Phaser.BitmapText.prototype.updateText * @private */ Phaser.BitmapText.prototype.updateText = function () { var data = this._data.font; if (!data) { return; } var text = this.text; var scale = this._fontSize / data.size; var lines = []; var y = 0; this.textWidth = 0; do { var line = this.scanLine(data, scale, text); line.y = y; lines.push(line); if (line.width > this.textWidth) { this.textWidth = line.width; } y += (data.lineHeight * scale); text = text.substr(line.text.length + 1); } while (line.end === false); this.textHeight = y; var t = 0; var align = 0; var ax = this.textWidth * this.anchor.x; var ay = this.textHeight * this.anchor.y; for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (this._align === 'right') { align = this.textWidth - line.width; } else if (this._align === 'center') { align = (this.textWidth - line.width) / 2; } for (var c = 0; c < line.text.length; c++) { var charCode = line.text.charCodeAt(c); var charData = data.chars[charCode]; var g = this._glyphs[t]; if (g) { // Sprite already exists in the glyphs pool, so we'll reuse it for this letter g.texture = charData.texture; // g.name = line.text[c]; // console.log('reusing', g.name, 'as', line.text[c]); } else { // We need a new sprite as the pool is empty or exhausted g = new PIXI.Sprite(charData.texture); g.name = line.text[c]; this._glyphs.push(g); // console.log('new', line.text[c]); } g.position.x = (line.chars[c] + align) - ax; g.position.y = (line.y + (charData.yOffset * scale)) - ay; g.scale.set(scale); g.tint = this.tint; if (!g.parent) { this.addChild(g); } t++; } } // Remove unnecessary children // This moves them from the display list (children array) but retains them in the _glyphs pool for (i = t; i < this._glyphs.length; i++) { this.removeChild(this._glyphs[i]); } }; /** * If a BitmapText changes from having a large number of characters to having very few characters it will cause lots of * Sprites to be retained in the BitmapText._glyphs array. Although they are not attached to the display list they * still take up memory while sat in the glyphs pool waiting to be re-used in the future. * * If you know that the BitmapText will not grow any larger then you can purge out the excess glyphs from the pool * by calling this method. * * Calling this doesn't prevent you from increasing the length of the text again in the future. * * @method Phaser.BitmapText.prototype.purgeGlyphs * @return {integer} The amount of glyphs removed from the pool. */ Phaser.BitmapText.prototype.purgeGlyphs = function () { var len = this._glyphs.length; var kept = []; for (var i = 0; i < this._glyphs.length; i++) { if (this._glyphs[i].parent !== this) { this._glyphs[i].destroy(); } else { kept.push(this._glyphs[i]); } } this._glyphs = []; this._glyphs = kept; this.updateText(); return len - kept.length; }; /** * Updates the transform of this object. * * @method Phaser.BitmapText.prototype.updateTransform * @private */ Phaser.BitmapText.prototype.updateTransform = function () { if (this.dirty || !this.anchor.equals(this._prevAnchor)) { this.updateText(); this.dirty = false; this._prevAnchor.copyFrom(this.anchor); } PIXI.DisplayObjectContainer.prototype.updateTransform.call(this); }; /** * @name Phaser.BitmapText#align * @property {string} align - Alignment for multi-line text ('left', 'center' or 'right'), does not affect single lines of text. */ Object.defineProperty(Phaser.BitmapText.prototype, 'align', { get: function() { return this._align; }, set: function(value) { if (value !== this._align && (value === 'left' || value === 'center' || value === 'right')) { this._align = value; this.updateText(); } } }); /** * @name Phaser.BitmapText#tint * @property {number} tint - The tint applied to the BitmapText. This is a hex value. Set to white to disable (0xFFFFFF) */ Object.defineProperty(Phaser.BitmapText.prototype, 'tint', { get: function() { return this._tint; }, set: function(value) { if (value !== this._tint) { this._tint = value; this.updateText(); } } }); /** * @name Phaser.BitmapText#font * @property {string} font - The font the text will be rendered in, i.e. 'Arial'. Must be loaded in the browser before use. */ Object.defineProperty(Phaser.BitmapText.prototype, 'font', { get: function() { return this._font; }, set: function(value) { if (value !== this._font) { this._font = value.trim(); this.updateText(); } } }); /** * @name Phaser.BitmapText#fontSize * @property {number} fontSize - The size of the font in pixels. */ Object.defineProperty(Phaser.BitmapText.prototype, 'fontSize', { get: function() { return this._fontSize; }, set: function(value) { value = parseInt(value, 10); if (value !== this._fontSize && value > 0) { this._fontSize = value; this.updateText(); } } }); /** * @name Phaser.BitmapText#text * @property {string} text - The text to be displayed by this BitmapText object. */ Object.defineProperty(Phaser.BitmapText.prototype, 'text', { get: function() { return this._text; }, set: function(value) { if (value !== this._text) { this._text = value.toString() || ''; this.updateText(); } } }); /** * The maximum display width of this BitmapText in pixels. * * If BitmapText.text is longer than maxWidth then the lines will be automatically wrapped * based on the last whitespace character found in the line. * * If no whitespace was found then no wrapping will take place and consequently the maxWidth value will not be honored. * * Disable maxWidth by setting the value to 0. * * @name Phaser.BitmapText#maxWidth * @property {number} maxWidth - The maximum width of this BitmapText in pixels. */ Object.defineProperty(Phaser.BitmapText.prototype, 'maxWidth', { get: function() { return this._maxWidth; }, set: function(value) { if (value !== this._maxWidth) { this._maxWidth = value; this.updateText(); } } });