Skip to content

Commit

Permalink
Fix issues with measuring with global styles and custom fonts
Browse files Browse the repository at this point in the history
  • Loading branch information
bigtimebuddy committed Dec 2, 2022
1 parent e11a424 commit 0d28826
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 30 deletions.
Binary file added demo/OpenSans-Bold.ttf
Binary file not shown.
Binary file added demo/OpenSans-Regular.ttf
Binary file not shown.
Binary file removed demo/Roboto-Bold.ttf
Binary file not shown.
Binary file removed demo/Roboto-Regular.ttf
Binary file not shown.
27 changes: 17 additions & 10 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ <h2>Internal Preview</h2>

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,
Expand All @@ -59,24 +59,31 @@ <h2>Internal Preview</h2>
const text = 'Lorem ipsum dolor sit amet, &#x1F680; <b>consectetur &nbsp; adipiscing elit</b>.<br>Phasellus porta&nbsp;nisi est, vitae <i>sagittis ex gravida ac</i>. 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);
</script>
</body>
</html>
25 changes: 18 additions & 7 deletions src/HTMLText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -73,13 +75,18 @@ 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;
this._foreignObject = foreignObject;
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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
}

/**
Expand Down
46 changes: 33 additions & 13 deletions src/HTMLTextStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ interface IHTMLTextStyle extends Omit<ITextStyle, ITextStyleIgnore>

interface IHTMLFont
{
url: string;
dataSrc: string;
src: string;
family: string;
weight: TextStyleFontWeight;
style: TextStyleFontStyle;
Expand Down Expand Up @@ -86,37 +87,50 @@ 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++;
}
}

/** Because of how HTMLText renders, fonts need to be imported */
public loadFont(url: string, options: Partial<Omit<IHTMLFont, 'url'>> = {}): Promise<IHTMLFont>
public loadFont(url: string, options: Partial<Omit<IHTMLFont, 'url'>> = {}): Promise<void>
{
return settings.ADAPTER.fetch(url)
return settings.ADAPTER.fetch(`${url}?v=${Date.now()}`)
.then((response) => response.blob())
.then((blob) => new Promise<string>((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++;
});
}

Expand Down Expand Up @@ -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};
}`
Expand Down Expand Up @@ -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;
}
Expand All @@ -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[])
{
Expand Down

0 comments on commit 0d28826

Please sign in to comment.