diff --git a/src/HTMLText.ts b/src/HTMLText.ts index 42fb56f..9fe3172 100644 --- a/src/HTMLText.ts +++ b/src/HTMLText.ts @@ -1,5 +1,5 @@ import { Sprite } from '@pixi/sprite'; -import { Texture, Rectangle, settings, utils, ICanvas, ICanvasRenderingContext2D } from '@pixi/core'; +import { Texture, Rectangle, settings, utils, ICanvas, ICanvasRenderingContext2D, ISize } from '@pixi/core'; import { TextStyle } from '@pixi/text'; import { HTMLTextStyle } from './HTMLTextStyle'; @@ -16,7 +16,13 @@ import type { IDestroyOptions } from '@pixi/display'; */ export class HTMLText extends Sprite { - /** Default opens when destroying */ + /** + * Default opens when destroying. + * @type {PIXI.IDestroyOptions} + * @property {boolean} texture=true - Whether to destroy the texture. + * @property {boolean} children=false - Whether to destroy the children. + * @property {boolean} baseTexture=true - Whether to destroy the base texture. + */ public static defaultDestroyOptions: IDestroyOptions = { texture: true, children: false, @@ -124,6 +130,56 @@ export class HTMLText extends Sprite this.style = style; } + /** + * Calculate the size of the output text without actually drawing it. + * This includes the `padding` in the `style` object. + * This can be used as a fast-pass to do things like text-fitting. + * @param {object} [overrides] - Overrides for the text, style, and resolution. + * @param {string} [overrides.text] - The text to measure, if not specified, the current text is used. + * @param {HTMLTextStyle} [overrides.style] - The style to measure, if not specified, the current style is used. + * @param {number} [overrides.resolution] - The resolution to measure, if not specified, the current resolution is used. + * @return {PIXI.ISize} Width and height of the measured text. + */ + measureText(overrides?: { text?: string, style?: HTMLTextStyle, resolution?: number }): ISize + { + const { text, style, resolution } = Object.assign({ + text: this._text, + style: this._style, + resolution: this._resolution, + }, overrides); + + Object.assign(this._domElement, { + innerHTML: text, + style: style.toCSS(resolution), + }); + this._styleElement.textContent = style.toGlobalCSS(); + + // Measure the contents using the shadow DOM + const contentBounds = this._domElement.getBoundingClientRect(); + + const contentWidth = Math.min(this.maxWidth, Math.ceil(contentBounds.width)); + const contentHeight = Math.min(this.maxHeight, Math.ceil(contentBounds.height)); + + this._svgRoot.setAttribute('width', contentWidth.toString()); + this._svgRoot.setAttribute('height', contentHeight.toString()); + + // Undo the changes to the DOM element + if (text !== this._text) + { + this._domElement.innerHTML = this._text as string; + } + if (style !== this._style) + { + Object.assign(this._domElement, { style: this._style?.toCSS(resolution) }); + this._styleElement.textContent = this._style?.toGlobalCSS() as string; + } + + return { + width: contentWidth + (style.padding * 2), + height: contentHeight + (style.padding * 2), + }; + } + /** * Manually refresh the text. * @public @@ -132,7 +188,7 @@ export class HTMLText extends Sprite */ updateText(respectDirty = true): void { - const { style, resolution, canvas, context } = this; + const { style, canvas, context } = this; // check if style has changed.. if (this.localStyleID !== style.styleID) @@ -146,23 +202,12 @@ export class HTMLText extends Sprite return; } - Object.assign(this._domElement, { - innerHTML: this._text, - style: style.toCSS(resolution), - }); - this._styleElement.textContent = style.toGlobalCSS(); - - // Measure the contents using the shadow DOM - const contentBounds = this._domElement.getBoundingClientRect(); - - const width = Math.min(this.maxWidth, Math.ceil(contentBounds.width)); - const height = Math.min(this.maxHeight, Math.ceil(contentBounds.height)); - - this._svgRoot.setAttribute('width', width.toString()); - this._svgRoot.setAttribute('height', height.toString()); + const { width, height } = this.measureText(); - canvas.width = Math.ceil((Math.max(1, width) + (style.padding * 2))); - canvas.height = Math.ceil((Math.max(1, height) + (style.padding * 2))); + // Make sure canvas is at least 1x1 so it drawable + // for sub-pixel sizes, round up to avoid clipping + canvas.width = Math.ceil((Math.max(1, width))); + canvas.height = Math.ceil((Math.max(1, height))); if (!this._loading) { diff --git a/test/HTMLText.test.ts b/test/HTMLText.test.ts index cb3b3b8..d3586d7 100644 --- a/test/HTMLText.test.ts +++ b/test/HTMLText.test.ts @@ -1,4 +1,5 @@ import { HTMLText } from '../src/HTMLText'; +import { HTMLTextStyle } from '../src/HTMLTextStyle'; describe('HTMLText', () => { @@ -43,4 +44,84 @@ describe('HTMLText', () => expect(document.querySelector(query)).toBeFalsy(); }); + + describe('measureText', () => + { + it('should measure default text', () => + { + const text = new HTMLText('Hello world!'); + const size = text.measureText(); + + expect(size).toBeTruthy(); + expect(size.width).toBeGreaterThan(0); + expect(size.height).toBeGreaterThan(0); + + text.destroy(); + }); + + it('should measure empty text to be drawable', () => + { + const text = new HTMLText(); + const size = text.measureText({ text: '' }); + + expect(size).toBeTruthy(); + expect(size.width).toBe(0); + expect(size.height).toBe(0); + + text.destroy(); + }); + + it('should measure override text', () => + { + const text = new HTMLText(); + const size = text.measureText({ text: 'Hello world!' }); + + expect(size).toBeTruthy(); + expect(size.width).toBeGreaterThan(0); + expect(size.height).toBeGreaterThan(0); + + text.destroy(); + }); + + it('should measure with resolution', () => + { + const text = new HTMLText('Hello world!'); + const size = text.measureText(); + const size2 = text.measureText({ resolution: 2 }); + + expect(Math.abs((size2.width / 2) - size.width)).toBeLessThanOrEqual(1); + expect(Math.abs((size2.height / 2) - size.height)).toBeLessThanOrEqual(1); + text.destroy(); + }); + + it('should apply override style', () => + { + const text = new HTMLText('Hello world!', { + fontSize: 12, + }); + const style = new HTMLTextStyle({ + fontSize: 24, + }); + const size = text.measureText(); + const size2 = text.measureText({ style }); + + expect(Math.abs((size2.width / 2) - size.width)).toBeLessThanOrEqual(1); + expect(Math.abs((size2.height / 2) - size.height)).toBeLessThanOrEqual(1); + text.destroy(); + }); + + it('should apply override style without touching styleID', () => + { + const text = new HTMLText('Hello world!'); + const styleId = text.style.styleID; + const style = new HTMLTextStyle(); + const style2Id = style.styleID; + + text.measureText(); + text.measureText({ style }); + expect(styleId).toBe(text.style.styleID); + expect(style2Id).toBe(style.styleID); + text.destroy(); + }); + }); });