diff --git a/packages/text-bitmap/src/BitmapText.js b/packages/text-bitmap/src/BitmapText.js index b9b535a..14e7f2b 100644 --- a/packages/text-bitmap/src/BitmapText.js +++ b/packages/text-bitmap/src/BitmapText.js @@ -114,6 +114,13 @@ this._maxLineHeight = 0; /** + * Letter spacing. This is useful for setting the space between characters. + * @member {number} + * @private + */ + this._letterSpacing = 0; + + /** * Text anchor. read-only * * @member {PIXI.ObservablePoint} @@ -143,49 +150,37 @@ const pos = new Point(); const chars = []; const lineWidths = []; + const text = this.text.replace(/(?:\r\n|\r)/g, '\n'); + const textLength = text.length; + const maxWidth = this._maxWidth * data.size / this._font.size; let prevCharCode = null; let lastLineWidth = 0; let maxLineWidth = 0; let line = 0; - let lastSpace = -1; - let lastSpaceWidth = 0; + let lastBreakPos = -1; + let lastBreakWidth = 0; let spacesRemoved = 0; let maxLineHeight = 0; - for (let i = 0; i < this.text.length; i++) + for (let i = 0; i < textLength; i++) { - const charCode = this.text.charCodeAt(i); + const charCode = text.charCodeAt(i); + const char = text.charAt(i); - if (/(\s)/.test(this.text.charAt(i))) + if (/(?:\s)/.test(char)) { - lastSpace = i; - lastSpaceWidth = lastLineWidth; + lastBreakPos = i; + lastBreakWidth = lastLineWidth; } - if (/(?:\r\n|\r|\n)/.test(this.text.charAt(i))) + if (char === '\r' || char === '\n') { lineWidths.push(lastLineWidth); maxLineWidth = Math.max(maxLineWidth, lastLineWidth); - line++; - - pos.x = 0; - pos.y += data.lineHeight; - prevCharCode = null; - continue; - } - - if (lastSpace !== -1 && this._maxWidth > 0 && pos.x * scale > this._maxWidth) - { - removeItems(chars, lastSpace - spacesRemoved, i - lastSpace); - i = lastSpace; - lastSpace = -1; + ++line; ++spacesRemoved; - lineWidths.push(lastSpaceWidth); - maxLineWidth = Math.max(maxLineWidth, lastSpaceWidth); - line++; - pos.x = 0; pos.y += data.lineHeight; prevCharCode = null; @@ -208,16 +203,42 @@ texture: charData.texture, line, charCode, - position: new Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new Point(pos.x + charData.xOffset + (this._letterSpacing / 2), pos.y + charData.yOffset), }); - lastLineWidth = pos.x + (charData.texture.width + charData.xOffset); - pos.x += charData.xAdvance; + pos.x += charData.xAdvance + this._letterSpacing; + lastLineWidth = pos.x; maxLineHeight = Math.max(maxLineHeight, (charData.yOffset + charData.texture.height)); prevCharCode = charCode; + + if (lastBreakPos !== -1 && maxWidth > 0 && pos.x > maxWidth) + { + ++spacesRemoved; + removeItems(chars, 1 + lastBreakPos - spacesRemoved, 1 + i - lastBreakPos); + i = lastBreakPos; + lastBreakPos = -1; + + lineWidths.push(lastBreakWidth); + maxLineWidth = Math.max(maxLineWidth, lastBreakWidth); + line++; + + pos.x = 0; + pos.y += data.lineHeight; + prevCharCode = null; + } } - lineWidths.push(lastLineWidth); - maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + const lastChar = text.charAt(text.length - 1); + + if (lastChar !== '\r' && lastChar !== '\n') + { + if (/(?:\s)/.test(lastChar)) + { + lastLineWidth = lastBreakWidth; + } + + lineWidths.push(lastLineWidth); + maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + } const lineAlignOffsets = []; @@ -491,6 +512,25 @@ } /** + * Additional space between characters. + * + * @member {number} + */ + get letterSpacing() + { + return this._letterSpacing; + } + + set letterSpacing(value) // eslint-disable-line require-jsdoc + { + if (this._letterSpacing !== value) + { + this._letterSpacing = value; + this.dirty = true; + } + } + + /** * The height of the overall text, different from fontSize, * which is defined in the style object. * diff --git a/packages/text-bitmap/src/BitmapText.js b/packages/text-bitmap/src/BitmapText.js index b9b535a..14e7f2b 100644 --- a/packages/text-bitmap/src/BitmapText.js +++ b/packages/text-bitmap/src/BitmapText.js @@ -114,6 +114,13 @@ this._maxLineHeight = 0; /** + * Letter spacing. This is useful for setting the space between characters. + * @member {number} + * @private + */ + this._letterSpacing = 0; + + /** * Text anchor. read-only * * @member {PIXI.ObservablePoint} @@ -143,49 +150,37 @@ const pos = new Point(); const chars = []; const lineWidths = []; + const text = this.text.replace(/(?:\r\n|\r)/g, '\n'); + const textLength = text.length; + const maxWidth = this._maxWidth * data.size / this._font.size; let prevCharCode = null; let lastLineWidth = 0; let maxLineWidth = 0; let line = 0; - let lastSpace = -1; - let lastSpaceWidth = 0; + let lastBreakPos = -1; + let lastBreakWidth = 0; let spacesRemoved = 0; let maxLineHeight = 0; - for (let i = 0; i < this.text.length; i++) + for (let i = 0; i < textLength; i++) { - const charCode = this.text.charCodeAt(i); + const charCode = text.charCodeAt(i); + const char = text.charAt(i); - if (/(\s)/.test(this.text.charAt(i))) + if (/(?:\s)/.test(char)) { - lastSpace = i; - lastSpaceWidth = lastLineWidth; + lastBreakPos = i; + lastBreakWidth = lastLineWidth; } - if (/(?:\r\n|\r|\n)/.test(this.text.charAt(i))) + if (char === '\r' || char === '\n') { lineWidths.push(lastLineWidth); maxLineWidth = Math.max(maxLineWidth, lastLineWidth); - line++; - - pos.x = 0; - pos.y += data.lineHeight; - prevCharCode = null; - continue; - } - - if (lastSpace !== -1 && this._maxWidth > 0 && pos.x * scale > this._maxWidth) - { - removeItems(chars, lastSpace - spacesRemoved, i - lastSpace); - i = lastSpace; - lastSpace = -1; + ++line; ++spacesRemoved; - lineWidths.push(lastSpaceWidth); - maxLineWidth = Math.max(maxLineWidth, lastSpaceWidth); - line++; - pos.x = 0; pos.y += data.lineHeight; prevCharCode = null; @@ -208,16 +203,42 @@ texture: charData.texture, line, charCode, - position: new Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new Point(pos.x + charData.xOffset + (this._letterSpacing / 2), pos.y + charData.yOffset), }); - lastLineWidth = pos.x + (charData.texture.width + charData.xOffset); - pos.x += charData.xAdvance; + pos.x += charData.xAdvance + this._letterSpacing; + lastLineWidth = pos.x; maxLineHeight = Math.max(maxLineHeight, (charData.yOffset + charData.texture.height)); prevCharCode = charCode; + + if (lastBreakPos !== -1 && maxWidth > 0 && pos.x > maxWidth) + { + ++spacesRemoved; + removeItems(chars, 1 + lastBreakPos - spacesRemoved, 1 + i - lastBreakPos); + i = lastBreakPos; + lastBreakPos = -1; + + lineWidths.push(lastBreakWidth); + maxLineWidth = Math.max(maxLineWidth, lastBreakWidth); + line++; + + pos.x = 0; + pos.y += data.lineHeight; + prevCharCode = null; + } } - lineWidths.push(lastLineWidth); - maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + const lastChar = text.charAt(text.length - 1); + + if (lastChar !== '\r' && lastChar !== '\n') + { + if (/(?:\s)/.test(lastChar)) + { + lastLineWidth = lastBreakWidth; + } + + lineWidths.push(lastLineWidth); + maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + } const lineAlignOffsets = []; @@ -491,6 +512,25 @@ } /** + * Additional space between characters. + * + * @member {number} + */ + get letterSpacing() + { + return this._letterSpacing; + } + + set letterSpacing(value) // eslint-disable-line require-jsdoc + { + if (this._letterSpacing !== value) + { + this._letterSpacing = value; + this.dirty = true; + } + } + + /** * The height of the overall text, different from fontSize, * which is defined in the style object. * diff --git a/packages/text-bitmap/test/BitmapText.js b/packages/text-bitmap/test/BitmapText.js new file mode 100644 index 0000000..ff7a82c --- /dev/null +++ b/packages/text-bitmap/test/BitmapText.js @@ -0,0 +1,104 @@ +const path = require('path'); +const fs = require('fs'); +const { BitmapText } = require('../'); +const { Texture, BaseTexture } = require('@pixi/core'); + +describe('PIXI.BitmapText', function () +{ + before(function (done) + { + this.fontXML = null; + this.fontImage = null; + this.font = null; + + const resolveURL = (url) => path.resolve(this.resources, url); + const loadXML = (url) => new Promise((resolve) => + fs.readFile(resolveURL(url), 'utf8', (err, data) => + { + expect(err).to.be.null; + resolve((new window.DOMParser()).parseFromString(data, 'text/xml')); + })); + + const loadImage = (url) => new Promise((resolve) => + { + const image = new Image(); + + image.onload = () => resolve(image); + image.src = resolveURL(url); + }); + + this.resources = path.join(__dirname, 'resources'); + Promise.all([ + loadXML('font1.fnt'), + loadImage('font1.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new Texture(new BaseTexture(this.fontImage, null, 1)); + + this.font = BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new BitmapText('', { + font: this.font.font, + size: 24, + }); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A '; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A'; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + }); + it('letterSpacing should add extra space between characters', function () + { + const text = 'ABCD zz DCBA'; + const bmpText = new BitmapText(text, { + font: this.font.font, + }); + const positions = []; + const renderedChars = bmpText.children.length; + + for (let x = 0; x < renderedChars; ++x) + { + positions.push(bmpText.children[x].x); + } + for (let space = 1; space < 20; ++space) + { + bmpText.letterSpacing = space; + bmpText.updateText(); + let prevPos = bmpText.children[0].x; + + for (let char = 1; char < renderedChars; ++char) + { + expect(bmpText.children[char].x).to.equal(prevPos + space + positions[char] - positions[char - 1]); + prevPos = bmpText.children[char].x; + } + } + }); + }); +}); diff --git a/packages/text-bitmap/src/BitmapText.js b/packages/text-bitmap/src/BitmapText.js index b9b535a..14e7f2b 100644 --- a/packages/text-bitmap/src/BitmapText.js +++ b/packages/text-bitmap/src/BitmapText.js @@ -114,6 +114,13 @@ this._maxLineHeight = 0; /** + * Letter spacing. This is useful for setting the space between characters. + * @member {number} + * @private + */ + this._letterSpacing = 0; + + /** * Text anchor. read-only * * @member {PIXI.ObservablePoint} @@ -143,49 +150,37 @@ const pos = new Point(); const chars = []; const lineWidths = []; + const text = this.text.replace(/(?:\r\n|\r)/g, '\n'); + const textLength = text.length; + const maxWidth = this._maxWidth * data.size / this._font.size; let prevCharCode = null; let lastLineWidth = 0; let maxLineWidth = 0; let line = 0; - let lastSpace = -1; - let lastSpaceWidth = 0; + let lastBreakPos = -1; + let lastBreakWidth = 0; let spacesRemoved = 0; let maxLineHeight = 0; - for (let i = 0; i < this.text.length; i++) + for (let i = 0; i < textLength; i++) { - const charCode = this.text.charCodeAt(i); + const charCode = text.charCodeAt(i); + const char = text.charAt(i); - if (/(\s)/.test(this.text.charAt(i))) + if (/(?:\s)/.test(char)) { - lastSpace = i; - lastSpaceWidth = lastLineWidth; + lastBreakPos = i; + lastBreakWidth = lastLineWidth; } - if (/(?:\r\n|\r|\n)/.test(this.text.charAt(i))) + if (char === '\r' || char === '\n') { lineWidths.push(lastLineWidth); maxLineWidth = Math.max(maxLineWidth, lastLineWidth); - line++; - - pos.x = 0; - pos.y += data.lineHeight; - prevCharCode = null; - continue; - } - - if (lastSpace !== -1 && this._maxWidth > 0 && pos.x * scale > this._maxWidth) - { - removeItems(chars, lastSpace - spacesRemoved, i - lastSpace); - i = lastSpace; - lastSpace = -1; + ++line; ++spacesRemoved; - lineWidths.push(lastSpaceWidth); - maxLineWidth = Math.max(maxLineWidth, lastSpaceWidth); - line++; - pos.x = 0; pos.y += data.lineHeight; prevCharCode = null; @@ -208,16 +203,42 @@ texture: charData.texture, line, charCode, - position: new Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new Point(pos.x + charData.xOffset + (this._letterSpacing / 2), pos.y + charData.yOffset), }); - lastLineWidth = pos.x + (charData.texture.width + charData.xOffset); - pos.x += charData.xAdvance; + pos.x += charData.xAdvance + this._letterSpacing; + lastLineWidth = pos.x; maxLineHeight = Math.max(maxLineHeight, (charData.yOffset + charData.texture.height)); prevCharCode = charCode; + + if (lastBreakPos !== -1 && maxWidth > 0 && pos.x > maxWidth) + { + ++spacesRemoved; + removeItems(chars, 1 + lastBreakPos - spacesRemoved, 1 + i - lastBreakPos); + i = lastBreakPos; + lastBreakPos = -1; + + lineWidths.push(lastBreakWidth); + maxLineWidth = Math.max(maxLineWidth, lastBreakWidth); + line++; + + pos.x = 0; + pos.y += data.lineHeight; + prevCharCode = null; + } } - lineWidths.push(lastLineWidth); - maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + const lastChar = text.charAt(text.length - 1); + + if (lastChar !== '\r' && lastChar !== '\n') + { + if (/(?:\s)/.test(lastChar)) + { + lastLineWidth = lastBreakWidth; + } + + lineWidths.push(lastLineWidth); + maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + } const lineAlignOffsets = []; @@ -491,6 +512,25 @@ } /** + * Additional space between characters. + * + * @member {number} + */ + get letterSpacing() + { + return this._letterSpacing; + } + + set letterSpacing(value) // eslint-disable-line require-jsdoc + { + if (this._letterSpacing !== value) + { + this._letterSpacing = value; + this.dirty = true; + } + } + + /** * The height of the overall text, different from fontSize, * which is defined in the style object. * diff --git a/packages/text-bitmap/test/BitmapText.js b/packages/text-bitmap/test/BitmapText.js new file mode 100644 index 0000000..ff7a82c --- /dev/null +++ b/packages/text-bitmap/test/BitmapText.js @@ -0,0 +1,104 @@ +const path = require('path'); +const fs = require('fs'); +const { BitmapText } = require('../'); +const { Texture, BaseTexture } = require('@pixi/core'); + +describe('PIXI.BitmapText', function () +{ + before(function (done) + { + this.fontXML = null; + this.fontImage = null; + this.font = null; + + const resolveURL = (url) => path.resolve(this.resources, url); + const loadXML = (url) => new Promise((resolve) => + fs.readFile(resolveURL(url), 'utf8', (err, data) => + { + expect(err).to.be.null; + resolve((new window.DOMParser()).parseFromString(data, 'text/xml')); + })); + + const loadImage = (url) => new Promise((resolve) => + { + const image = new Image(); + + image.onload = () => resolve(image); + image.src = resolveURL(url); + }); + + this.resources = path.join(__dirname, 'resources'); + Promise.all([ + loadXML('font1.fnt'), + loadImage('font1.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new Texture(new BaseTexture(this.fontImage, null, 1)); + + this.font = BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new BitmapText('', { + font: this.font.font, + size: 24, + }); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A '; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A'; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + }); + it('letterSpacing should add extra space between characters', function () + { + const text = 'ABCD zz DCBA'; + const bmpText = new BitmapText(text, { + font: this.font.font, + }); + const positions = []; + const renderedChars = bmpText.children.length; + + for (let x = 0; x < renderedChars; ++x) + { + positions.push(bmpText.children[x].x); + } + for (let space = 1; space < 20; ++space) + { + bmpText.letterSpacing = space; + bmpText.updateText(); + let prevPos = bmpText.children[0].x; + + for (let char = 1; char < renderedChars; ++char) + { + expect(bmpText.children[char].x).to.equal(prevPos + space + positions[char] - positions[char - 1]); + prevPos = bmpText.children[char].x; + } + } + }); + }); +}); diff --git a/packages/text-bitmap/test/index.js b/packages/text-bitmap/test/index.js index 688fee2..6fab55e 100644 --- a/packages/text-bitmap/test/index.js +++ b/packages/text-bitmap/test/index.js @@ -1 +1,2 @@ +require('./BitmapText'); require('./BitmapFontLoader'); diff --git a/packages/text-bitmap/src/BitmapText.js b/packages/text-bitmap/src/BitmapText.js index b9b535a..14e7f2b 100644 --- a/packages/text-bitmap/src/BitmapText.js +++ b/packages/text-bitmap/src/BitmapText.js @@ -114,6 +114,13 @@ this._maxLineHeight = 0; /** + * Letter spacing. This is useful for setting the space between characters. + * @member {number} + * @private + */ + this._letterSpacing = 0; + + /** * Text anchor. read-only * * @member {PIXI.ObservablePoint} @@ -143,49 +150,37 @@ const pos = new Point(); const chars = []; const lineWidths = []; + const text = this.text.replace(/(?:\r\n|\r)/g, '\n'); + const textLength = text.length; + const maxWidth = this._maxWidth * data.size / this._font.size; let prevCharCode = null; let lastLineWidth = 0; let maxLineWidth = 0; let line = 0; - let lastSpace = -1; - let lastSpaceWidth = 0; + let lastBreakPos = -1; + let lastBreakWidth = 0; let spacesRemoved = 0; let maxLineHeight = 0; - for (let i = 0; i < this.text.length; i++) + for (let i = 0; i < textLength; i++) { - const charCode = this.text.charCodeAt(i); + const charCode = text.charCodeAt(i); + const char = text.charAt(i); - if (/(\s)/.test(this.text.charAt(i))) + if (/(?:\s)/.test(char)) { - lastSpace = i; - lastSpaceWidth = lastLineWidth; + lastBreakPos = i; + lastBreakWidth = lastLineWidth; } - if (/(?:\r\n|\r|\n)/.test(this.text.charAt(i))) + if (char === '\r' || char === '\n') { lineWidths.push(lastLineWidth); maxLineWidth = Math.max(maxLineWidth, lastLineWidth); - line++; - - pos.x = 0; - pos.y += data.lineHeight; - prevCharCode = null; - continue; - } - - if (lastSpace !== -1 && this._maxWidth > 0 && pos.x * scale > this._maxWidth) - { - removeItems(chars, lastSpace - spacesRemoved, i - lastSpace); - i = lastSpace; - lastSpace = -1; + ++line; ++spacesRemoved; - lineWidths.push(lastSpaceWidth); - maxLineWidth = Math.max(maxLineWidth, lastSpaceWidth); - line++; - pos.x = 0; pos.y += data.lineHeight; prevCharCode = null; @@ -208,16 +203,42 @@ texture: charData.texture, line, charCode, - position: new Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new Point(pos.x + charData.xOffset + (this._letterSpacing / 2), pos.y + charData.yOffset), }); - lastLineWidth = pos.x + (charData.texture.width + charData.xOffset); - pos.x += charData.xAdvance; + pos.x += charData.xAdvance + this._letterSpacing; + lastLineWidth = pos.x; maxLineHeight = Math.max(maxLineHeight, (charData.yOffset + charData.texture.height)); prevCharCode = charCode; + + if (lastBreakPos !== -1 && maxWidth > 0 && pos.x > maxWidth) + { + ++spacesRemoved; + removeItems(chars, 1 + lastBreakPos - spacesRemoved, 1 + i - lastBreakPos); + i = lastBreakPos; + lastBreakPos = -1; + + lineWidths.push(lastBreakWidth); + maxLineWidth = Math.max(maxLineWidth, lastBreakWidth); + line++; + + pos.x = 0; + pos.y += data.lineHeight; + prevCharCode = null; + } } - lineWidths.push(lastLineWidth); - maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + const lastChar = text.charAt(text.length - 1); + + if (lastChar !== '\r' && lastChar !== '\n') + { + if (/(?:\s)/.test(lastChar)) + { + lastLineWidth = lastBreakWidth; + } + + lineWidths.push(lastLineWidth); + maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + } const lineAlignOffsets = []; @@ -491,6 +512,25 @@ } /** + * Additional space between characters. + * + * @member {number} + */ + get letterSpacing() + { + return this._letterSpacing; + } + + set letterSpacing(value) // eslint-disable-line require-jsdoc + { + if (this._letterSpacing !== value) + { + this._letterSpacing = value; + this.dirty = true; + } + } + + /** * The height of the overall text, different from fontSize, * which is defined in the style object. * diff --git a/packages/text-bitmap/test/BitmapText.js b/packages/text-bitmap/test/BitmapText.js new file mode 100644 index 0000000..ff7a82c --- /dev/null +++ b/packages/text-bitmap/test/BitmapText.js @@ -0,0 +1,104 @@ +const path = require('path'); +const fs = require('fs'); +const { BitmapText } = require('../'); +const { Texture, BaseTexture } = require('@pixi/core'); + +describe('PIXI.BitmapText', function () +{ + before(function (done) + { + this.fontXML = null; + this.fontImage = null; + this.font = null; + + const resolveURL = (url) => path.resolve(this.resources, url); + const loadXML = (url) => new Promise((resolve) => + fs.readFile(resolveURL(url), 'utf8', (err, data) => + { + expect(err).to.be.null; + resolve((new window.DOMParser()).parseFromString(data, 'text/xml')); + })); + + const loadImage = (url) => new Promise((resolve) => + { + const image = new Image(); + + image.onload = () => resolve(image); + image.src = resolveURL(url); + }); + + this.resources = path.join(__dirname, 'resources'); + Promise.all([ + loadXML('font1.fnt'), + loadImage('font1.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new Texture(new BaseTexture(this.fontImage, null, 1)); + + this.font = BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new BitmapText('', { + font: this.font.font, + size: 24, + }); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A '; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A'; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + }); + it('letterSpacing should add extra space between characters', function () + { + const text = 'ABCD zz DCBA'; + const bmpText = new BitmapText(text, { + font: this.font.font, + }); + const positions = []; + const renderedChars = bmpText.children.length; + + for (let x = 0; x < renderedChars; ++x) + { + positions.push(bmpText.children[x].x); + } + for (let space = 1; space < 20; ++space) + { + bmpText.letterSpacing = space; + bmpText.updateText(); + let prevPos = bmpText.children[0].x; + + for (let char = 1; char < renderedChars; ++char) + { + expect(bmpText.children[char].x).to.equal(prevPos + space + positions[char] - positions[char - 1]); + prevPos = bmpText.children[char].x; + } + } + }); + }); +}); diff --git a/packages/text-bitmap/test/index.js b/packages/text-bitmap/test/index.js index 688fee2..6fab55e 100644 --- a/packages/text-bitmap/test/index.js +++ b/packages/text-bitmap/test/index.js @@ -1 +1,2 @@ +require('./BitmapText'); require('./BitmapFontLoader'); diff --git a/packages/text-bitmap/test/resources/font1.fnt b/packages/text-bitmap/test/resources/font1.fnt new file mode 100644 index 0000000..74496bb --- /dev/null +++ b/packages/text-bitmap/test/resources/font1.fnt @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/text-bitmap/src/BitmapText.js b/packages/text-bitmap/src/BitmapText.js index b9b535a..14e7f2b 100644 --- a/packages/text-bitmap/src/BitmapText.js +++ b/packages/text-bitmap/src/BitmapText.js @@ -114,6 +114,13 @@ this._maxLineHeight = 0; /** + * Letter spacing. This is useful for setting the space between characters. + * @member {number} + * @private + */ + this._letterSpacing = 0; + + /** * Text anchor. read-only * * @member {PIXI.ObservablePoint} @@ -143,49 +150,37 @@ const pos = new Point(); const chars = []; const lineWidths = []; + const text = this.text.replace(/(?:\r\n|\r)/g, '\n'); + const textLength = text.length; + const maxWidth = this._maxWidth * data.size / this._font.size; let prevCharCode = null; let lastLineWidth = 0; let maxLineWidth = 0; let line = 0; - let lastSpace = -1; - let lastSpaceWidth = 0; + let lastBreakPos = -1; + let lastBreakWidth = 0; let spacesRemoved = 0; let maxLineHeight = 0; - for (let i = 0; i < this.text.length; i++) + for (let i = 0; i < textLength; i++) { - const charCode = this.text.charCodeAt(i); + const charCode = text.charCodeAt(i); + const char = text.charAt(i); - if (/(\s)/.test(this.text.charAt(i))) + if (/(?:\s)/.test(char)) { - lastSpace = i; - lastSpaceWidth = lastLineWidth; + lastBreakPos = i; + lastBreakWidth = lastLineWidth; } - if (/(?:\r\n|\r|\n)/.test(this.text.charAt(i))) + if (char === '\r' || char === '\n') { lineWidths.push(lastLineWidth); maxLineWidth = Math.max(maxLineWidth, lastLineWidth); - line++; - - pos.x = 0; - pos.y += data.lineHeight; - prevCharCode = null; - continue; - } - - if (lastSpace !== -1 && this._maxWidth > 0 && pos.x * scale > this._maxWidth) - { - removeItems(chars, lastSpace - spacesRemoved, i - lastSpace); - i = lastSpace; - lastSpace = -1; + ++line; ++spacesRemoved; - lineWidths.push(lastSpaceWidth); - maxLineWidth = Math.max(maxLineWidth, lastSpaceWidth); - line++; - pos.x = 0; pos.y += data.lineHeight; prevCharCode = null; @@ -208,16 +203,42 @@ texture: charData.texture, line, charCode, - position: new Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new Point(pos.x + charData.xOffset + (this._letterSpacing / 2), pos.y + charData.yOffset), }); - lastLineWidth = pos.x + (charData.texture.width + charData.xOffset); - pos.x += charData.xAdvance; + pos.x += charData.xAdvance + this._letterSpacing; + lastLineWidth = pos.x; maxLineHeight = Math.max(maxLineHeight, (charData.yOffset + charData.texture.height)); prevCharCode = charCode; + + if (lastBreakPos !== -1 && maxWidth > 0 && pos.x > maxWidth) + { + ++spacesRemoved; + removeItems(chars, 1 + lastBreakPos - spacesRemoved, 1 + i - lastBreakPos); + i = lastBreakPos; + lastBreakPos = -1; + + lineWidths.push(lastBreakWidth); + maxLineWidth = Math.max(maxLineWidth, lastBreakWidth); + line++; + + pos.x = 0; + pos.y += data.lineHeight; + prevCharCode = null; + } } - lineWidths.push(lastLineWidth); - maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + const lastChar = text.charAt(text.length - 1); + + if (lastChar !== '\r' && lastChar !== '\n') + { + if (/(?:\s)/.test(lastChar)) + { + lastLineWidth = lastBreakWidth; + } + + lineWidths.push(lastLineWidth); + maxLineWidth = Math.max(maxLineWidth, lastLineWidth); + } const lineAlignOffsets = []; @@ -491,6 +512,25 @@ } /** + * Additional space between characters. + * + * @member {number} + */ + get letterSpacing() + { + return this._letterSpacing; + } + + set letterSpacing(value) // eslint-disable-line require-jsdoc + { + if (this._letterSpacing !== value) + { + this._letterSpacing = value; + this.dirty = true; + } + } + + /** * The height of the overall text, different from fontSize, * which is defined in the style object. * diff --git a/packages/text-bitmap/test/BitmapText.js b/packages/text-bitmap/test/BitmapText.js new file mode 100644 index 0000000..ff7a82c --- /dev/null +++ b/packages/text-bitmap/test/BitmapText.js @@ -0,0 +1,104 @@ +const path = require('path'); +const fs = require('fs'); +const { BitmapText } = require('../'); +const { Texture, BaseTexture } = require('@pixi/core'); + +describe('PIXI.BitmapText', function () +{ + before(function (done) + { + this.fontXML = null; + this.fontImage = null; + this.font = null; + + const resolveURL = (url) => path.resolve(this.resources, url); + const loadXML = (url) => new Promise((resolve) => + fs.readFile(resolveURL(url), 'utf8', (err, data) => + { + expect(err).to.be.null; + resolve((new window.DOMParser()).parseFromString(data, 'text/xml')); + })); + + const loadImage = (url) => new Promise((resolve) => + { + const image = new Image(); + + image.onload = () => resolve(image); + image.src = resolveURL(url); + }); + + this.resources = path.join(__dirname, 'resources'); + Promise.all([ + loadXML('font1.fnt'), + loadImage('font1.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new Texture(new BaseTexture(this.fontImage, null, 1)); + + this.font = BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new BitmapText('', { + font: this.font.font, + size: 24, + }); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A '; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + + bmpText.maxWidth = 40; + bmpText.text = 'A A A A A A A'; + bmpText.updateText(); + + expect(bmpText.textWidth).to.lessThan(bmpText.maxWidth); + }); + it('letterSpacing should add extra space between characters', function () + { + const text = 'ABCD zz DCBA'; + const bmpText = new BitmapText(text, { + font: this.font.font, + }); + const positions = []; + const renderedChars = bmpText.children.length; + + for (let x = 0; x < renderedChars; ++x) + { + positions.push(bmpText.children[x].x); + } + for (let space = 1; space < 20; ++space) + { + bmpText.letterSpacing = space; + bmpText.updateText(); + let prevPos = bmpText.children[0].x; + + for (let char = 1; char < renderedChars; ++char) + { + expect(bmpText.children[char].x).to.equal(prevPos + space + positions[char] - positions[char - 1]); + prevPos = bmpText.children[char].x; + } + } + }); + }); +}); diff --git a/packages/text-bitmap/test/index.js b/packages/text-bitmap/test/index.js index 688fee2..6fab55e 100644 --- a/packages/text-bitmap/test/index.js +++ b/packages/text-bitmap/test/index.js @@ -1 +1,2 @@ +require('./BitmapText'); require('./BitmapFontLoader'); diff --git a/packages/text-bitmap/test/resources/font1.fnt b/packages/text-bitmap/test/resources/font1.fnt new file mode 100644 index 0000000..74496bb --- /dev/null +++ b/packages/text-bitmap/test/resources/font1.fnt @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/text-bitmap/test/resources/font1.png b/packages/text-bitmap/test/resources/font1.png new file mode 100644 index 0000000..d30d645 --- /dev/null +++ b/packages/text-bitmap/test/resources/font1.png Binary files differ