diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index b505250..a8b680e 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -111,6 +111,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} @@ -140,49 +147,37 @@ const pos = new core.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) - { - core.utils.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; @@ -205,16 +200,42 @@ texture: charData.texture, line, charCode, - position: new core.Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new core.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; + core.utils.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 = []; @@ -485,6 +506,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/src/extras/BitmapText.js b/src/extras/BitmapText.js index b505250..a8b680e 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -111,6 +111,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} @@ -140,49 +147,37 @@ const pos = new core.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) - { - core.utils.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; @@ -205,16 +200,42 @@ texture: charData.texture, line, charCode, - position: new core.Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new core.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; + core.utils.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 = []; @@ -485,6 +506,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/test/extras/BitmapText.js b/test/extras/BitmapText.js new file mode 100644 index 0000000..9d8d2b8 --- /dev/null +++ b/test/extras/BitmapText.js @@ -0,0 +1,104 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +describe('PIXI.extras.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('font.fnt'), + loadImage('font.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new PIXI.Texture(new PIXI.BaseTexture(this.fontImage, null, 1)); + + this.font = PIXI.extras.BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new PIXI.extras.BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new PIXI.extras.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 PIXI.extras.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/src/extras/BitmapText.js b/src/extras/BitmapText.js index b505250..a8b680e 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -111,6 +111,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} @@ -140,49 +147,37 @@ const pos = new core.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) - { - core.utils.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; @@ -205,16 +200,42 @@ texture: charData.texture, line, charCode, - position: new core.Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new core.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; + core.utils.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 = []; @@ -485,6 +506,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/test/extras/BitmapText.js b/test/extras/BitmapText.js new file mode 100644 index 0000000..9d8d2b8 --- /dev/null +++ b/test/extras/BitmapText.js @@ -0,0 +1,104 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +describe('PIXI.extras.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('font.fnt'), + loadImage('font.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new PIXI.Texture(new PIXI.BaseTexture(this.fontImage, null, 1)); + + this.font = PIXI.extras.BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new PIXI.extras.BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new PIXI.extras.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 PIXI.extras.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/test/extras/index.js b/test/extras/index.js new file mode 100644 index 0000000..48e56c2 --- /dev/null +++ b/test/extras/index.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./BitmapText'); diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index b505250..a8b680e 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -111,6 +111,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} @@ -140,49 +147,37 @@ const pos = new core.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) - { - core.utils.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; @@ -205,16 +200,42 @@ texture: charData.texture, line, charCode, - position: new core.Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new core.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; + core.utils.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 = []; @@ -485,6 +506,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/test/extras/BitmapText.js b/test/extras/BitmapText.js new file mode 100644 index 0000000..9d8d2b8 --- /dev/null +++ b/test/extras/BitmapText.js @@ -0,0 +1,104 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +describe('PIXI.extras.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('font.fnt'), + loadImage('font.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new PIXI.Texture(new PIXI.BaseTexture(this.fontImage, null, 1)); + + this.font = PIXI.extras.BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new PIXI.extras.BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new PIXI.extras.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 PIXI.extras.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/test/extras/index.js b/test/extras/index.js new file mode 100644 index 0000000..48e56c2 --- /dev/null +++ b/test/extras/index.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./BitmapText'); diff --git a/test/extras/resources/font.fnt b/test/extras/resources/font.fnt new file mode 100644 index 0000000..74496bb --- /dev/null +++ b/test/extras/resources/font.fnt @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index b505250..a8b680e 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -111,6 +111,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} @@ -140,49 +147,37 @@ const pos = new core.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) - { - core.utils.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; @@ -205,16 +200,42 @@ texture: charData.texture, line, charCode, - position: new core.Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new core.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; + core.utils.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 = []; @@ -485,6 +506,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/test/extras/BitmapText.js b/test/extras/BitmapText.js new file mode 100644 index 0000000..9d8d2b8 --- /dev/null +++ b/test/extras/BitmapText.js @@ -0,0 +1,104 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +describe('PIXI.extras.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('font.fnt'), + loadImage('font.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new PIXI.Texture(new PIXI.BaseTexture(this.fontImage, null, 1)); + + this.font = PIXI.extras.BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new PIXI.extras.BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new PIXI.extras.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 PIXI.extras.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/test/extras/index.js b/test/extras/index.js new file mode 100644 index 0000000..48e56c2 --- /dev/null +++ b/test/extras/index.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./BitmapText'); diff --git a/test/extras/resources/font.fnt b/test/extras/resources/font.fnt new file mode 100644 index 0000000..74496bb --- /dev/null +++ b/test/extras/resources/font.fnt @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/extras/resources/font.png b/test/extras/resources/font.png new file mode 100644 index 0000000..d30d645 --- /dev/null +++ b/test/extras/resources/font.png Binary files differ diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index b505250..a8b680e 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -111,6 +111,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} @@ -140,49 +147,37 @@ const pos = new core.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) - { - core.utils.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; @@ -205,16 +200,42 @@ texture: charData.texture, line, charCode, - position: new core.Point(pos.x + charData.xOffset, pos.y + charData.yOffset), + position: new core.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; + core.utils.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 = []; @@ -485,6 +506,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/test/extras/BitmapText.js b/test/extras/BitmapText.js new file mode 100644 index 0000000..9d8d2b8 --- /dev/null +++ b/test/extras/BitmapText.js @@ -0,0 +1,104 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +describe('PIXI.extras.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('font.fnt'), + loadImage('font.png'), + ]).then(([ + fontXML, + fontImage, + ]) => + { + this.fontXML = fontXML; + this.fontImage = fontImage; + const texture = new PIXI.Texture(new PIXI.BaseTexture(this.fontImage, null, 1)); + + this.font = PIXI.extras.BitmapText.registerFont(this.fontXML, texture); + done(); + }); + }); + + describe('text', function () + { + it('should render text even if there are unsupported characters', function () + { + const text = new PIXI.extras.BitmapText('ABCDEFG', { + font: this.font.font, + }); + + expect(text.children.length).to.equal(4); + }); + it('should break line on space', function () + { + const bmpText = new PIXI.extras.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 PIXI.extras.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/test/extras/index.js b/test/extras/index.js new file mode 100644 index 0000000..48e56c2 --- /dev/null +++ b/test/extras/index.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./BitmapText'); diff --git a/test/extras/resources/font.fnt b/test/extras/resources/font.fnt new file mode 100644 index 0000000..74496bb --- /dev/null +++ b/test/extras/resources/font.fnt @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/extras/resources/font.png b/test/extras/resources/font.png new file mode 100644 index 0000000..d30d645 --- /dev/null +++ b/test/extras/resources/font.png Binary files differ diff --git a/test/index.js b/test/index.js index 9689aa0..faf2b18 100755 --- a/test/index.js +++ b/test/index.js @@ -16,4 +16,5 @@ require('./loaders'); require('./renders'); require('./prepare'); + require('./extras'); });