diff --git a/demo/OpenSans-Bold.ttf b/demo/OpenSans-Bold.ttf new file mode 100644 index 0000000..a1398b3 Binary files /dev/null and b/demo/OpenSans-Bold.ttf differ diff --git a/demo/OpenSans-Regular.ttf b/demo/OpenSans-Regular.ttf new file mode 100644 index 0000000..1dc226d Binary files /dev/null and b/demo/OpenSans-Regular.ttf differ diff --git a/demo/Roboto-Bold.ttf b/demo/Roboto-Bold.ttf deleted file mode 100644 index 43da14d..0000000 Binary files a/demo/Roboto-Bold.ttf and /dev/null differ diff --git a/demo/Roboto-Regular.ttf b/demo/Roboto-Regular.ttf deleted file mode 100644 index ddf4bfa..0000000 Binary files a/demo/Roboto-Regular.ttf and /dev/null differ diff --git a/demo/index.html b/demo/index.html index 393dc44..731da81 100644 --- a/demo/index.html +++ b/demo/index.html @@ -41,9 +41,9 @@

Internal Preview

const style = new PIXI.HTMLTextStyle({ fontSize: 40, - fontFamily: ['Roboto', 'Arial'], + fontFamily: ['OpenSans', 'Courier New'], align: 'justify', - letterSpacing: 3, + letterSpacing: 1, wordWrap: true, wordWrapWidth: 600, lineHeight: 48, @@ -59,24 +59,31 @@

Internal Preview

const text = 'Lorem ipsum dolor sit amet, 🚀 consectetur   adipiscing elit.
Phasellus porta nisi est, vitae sagittis ex gravida ac. Sed vitae malesuada neque.'; const text2 = new PIXI.HTMLText(text, style); - text2.texture.baseTexture.on('update', () => app.render()); + text2.texture.baseTexture.on('update', () => { + app.render(); + refreshBounds(); + }); text2.y = 20; text2.x = 20; // Load the font weights Promise.all([ - text2.style.loadFont('./Roboto-Regular.ttf', { family: 'Roboto' }), - text2.style.loadFont('./Roboto-Bold.ttf', { family: 'Roboto', weight: 'bold' }), + text2.style.loadFont('./OpenSans-Regular.ttf', { family: 'OpenSans' }), + text2.style.loadFont('./OpenSans-Bold.ttf', { family: 'OpenSans', weight: 'bold' }), ]).then(() => app.render()); document.getElementById('text').appendChild(text2.canvas); + const rect = new PIXI.Graphics(); + + const refreshBounds = () => rect + .clear() + .beginFill(0, 0.01) + .lineStyle({ color: 0xffffff, width: 1, native: true }) + .drawRect(text2.x, text2.y, text2.width, text2.height); - const rect = new PIXI.Graphics() - .beginFill(0, 0) - .lineStyle({ color: 0xff0000, width: 1, native: true }) - .drawShape(text2.getBounds()); + refreshBounds(); - app.stage.addChild(rect, text2); + app.stage.addChild(text2, rect); \ No newline at end of file diff --git a/src/HTMLText.ts b/src/HTMLText.ts index c571876..3d036b7 100644 --- a/src/HTMLText.ts +++ b/src/HTMLText.ts @@ -34,6 +34,8 @@ export class HTMLText extends Sprite private _style: HTMLTextStyle | null = null; private _autoResolution = true; private _loading = false; + private _shadow: HTMLElement; + private _shadowRoot: ShadowRoot; private localStyleID = -1; private dirty = false; @@ -73,6 +75,9 @@ export class HTMLText extends Sprite svgRoot.appendChild(foreignObject); svgRoot.style.paintOrder = 'stroke fill'; + this._shadow = document.createElement('div'); + this._shadow.dataset.pixiId = 'text-html-shadow'; + this._shadowRoot = this._shadow.attachShadow({ mode: 'open' }); this._domElement = domElement; this._styleElement = styleElement; this._svgRoot = svgRoot; @@ -80,6 +85,8 @@ export class HTMLText extends Sprite this._image = new Image(); this._autoResolution = HTMLText.defaultAutoResolution; + document.body.appendChild(this._shadow); + this.canvas = canvas; this.context = canvas.getContext('2d') as ICanvasRenderingContext2D; this._resolution = HTMLText.defaultResolution ?? settings.RESOLUTION; @@ -118,22 +125,24 @@ export class HTMLText extends Sprite }); globalStyles.innerHTML = style.toGlobalCSS(); - // Measure the contents - document.body.appendChild(dom); + // Measure the contents using the shadow DOM + // we do this for CSS isolation + this._shadowRoot.appendChild(dom); + this._shadowRoot.appendChild(globalStyles); const { width: _width, height: _height } = dom.getBoundingClientRect(); const width = Math.ceil(_width); const height = Math.ceil(_height); - document.body.removeChild(dom); - // Assemble the svg output - this._foreignObject.appendChild(globalStyles); this._foreignObject.appendChild(dom); + this._foreignObject.appendChild(globalStyles); + + // console.log('getBBox', this._svgRoot.getBBox()); this._svgRoot.setAttribute('width', width.toString()); this._svgRoot.setAttribute('height', height.toString()); - canvas.width = Math.ceil((Math.max(1, width) + (style.padding * 2)) * resolution); - canvas.height = Math.ceil((Math.max(1, height) + (style.padding * 2)) * resolution); + canvas.width = Math.ceil((Math.max(1, width) + (style.padding * 2))); + canvas.height = Math.ceil((Math.max(1, height) + (style.padding * 2))); if (!this._loading) { @@ -301,6 +310,8 @@ export class HTMLText extends Sprite this._image.onload = null; this._image.src = ''; this._image = forceClear; + this._shadow = forceClear; + this._shadowRoot = forceClear; } /** diff --git a/src/HTMLTextStyle.ts b/src/HTMLTextStyle.ts index 02d4229..7bbfaf1 100644 --- a/src/HTMLTextStyle.ts +++ b/src/HTMLTextStyle.ts @@ -15,7 +15,8 @@ interface IHTMLTextStyle extends Omit interface IHTMLFont { - url: string; + dataSrc: string; + src: string; family: string; weight: TextStyleFontWeight; style: TextStyleFontStyle; @@ -86,6 +87,7 @@ class HTMLTextStyle extends TextStyle { if (this._fonts.length > 0) { + this._fonts.forEach(({ src }) => URL.revokeObjectURL(src)); this.fontFamily = 'Arial'; this._fonts.length = 0; this.styleID++; @@ -93,30 +95,42 @@ class HTMLTextStyle extends TextStyle } /** Because of how HTMLText renders, fonts need to be imported */ - public loadFont(url: string, options: Partial> = {}): Promise + public loadFont(url: string, options: Partial> = {}): Promise { - return settings.ADAPTER.fetch(url) + return settings.ADAPTER.fetch(`${url}?v=${Date.now()}`) .then((response) => response.blob()) - .then((blob) => new Promise((resolve, reject) => + .then(async (blob) => new Promise<[string, string]>((resolve, reject) => { + const src = URL.createObjectURL(blob); const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); + reader.onload = () => resolve([src, reader.result as string]); reader.onerror = reject; reader.readAsDataURL(blob); })) - .then((url) => + .then(async ([src, dataSrc]) => { const font = Object.assign({}, { family: utils.path.basename(url, utils.path.extname(url)), weight: 'normal', style: 'normal', - }, { url }, options) as IHTMLFont; + src, + dataSrc, + }, options) as IHTMLFont; this._fonts.push(font); this.styleID++; - return font; + // Load it into the current DOM so we can properly measure it! + const fontFace = new FontFace(font.family, `url(${font.src})`, { + weight: font.weight, + style: font.style, + }); + + await fontFace.load(); + document.fonts.add(fontFace); + await document.fonts.ready; + this.styleID++; }); } @@ -199,7 +213,7 @@ class HTMLTextStyle extends TextStyle `${result} @font-face { font-family: "${font.family}"; - src: url('${font.url}'); + src: url('${font.dataSrc}'); font-weight: ${font.weight}; font-style: ${font.style}; }` @@ -236,10 +250,8 @@ class HTMLTextStyle extends TextStyle color += (alpha * 255 | 0).toString(16).padStart(2, '0'); } - const { userAgent } = settings.ADAPTER.getNavigator(); - - // Shadow is flipped on Safari - if ((/^((?!chrome|android).)*safari/i).test(userAgent)) + // Hack: text-shadow is flipped on Safari, boo! + if (this.isSafari) { y *= -1; } @@ -260,6 +272,14 @@ class HTMLTextStyle extends TextStyle Object.assign(this, HTMLTextStyle.defaultOptions); } + /** Proving that Safari is the new IE */ + private get isSafari(): boolean + { + const { userAgent } = settings.ADAPTER.getNavigator(); + + return (/^((?!chrome|android).)*safari/i).test(userAgent); + } + /** @ignore fillGradientStops is not supported by HTMLText */ override set fillGradientStops(_value: number[]) {