Search code examples
javascriptfontshtml5-canvas

Error in first computation of text width using HTML5 measureText() with a custom font


I'm building a 2D top-down turn-based soccer game. Every time you click a player of your team, a card is drawn on a side canvas. I've just started the code for such cards and as a preliminary action it just draws the template with the selected player's name horizontally centered:

drawPlayerCard = (card, player) => {
    let ctx = this.leftUserContext;
    ctx.drawImage(card.template, card.position.x, card.position.y);
    const nameWidth = ctx.measureText(player.name).width;
    ctx.font = "12px fff";
    ctx.fillText(player.name, ((card.template.width - nameWidth) / 2) + card.position.x, (202 + card.position.y));
}

The font is a free font I downloaded and put in use through CSS

@font-face {
    font-family: fff;
    src: url(../font/fff-forward.regular.ttf);
}

body {
    font-family: fff;
    margin: 0;
    background-color: white;
}

and an old HTML trick I've found here on SO to get the font to preload:

<div id="TableSoccer">
    <canvas class="board" id="Field" width="1488" height="1008"></canvas>
    <canvas class="board" id="Players" width="1488" height="1008"></canvas>
    <canvas class="board" id="MouseEvents" width="1488" height="1008"></canvas>
    <canvas class="user left" id="LeftUser" width="200" height="1008"></canvas>
    <canvas class="user right" id="RightUser" width="200" height="1008"></canvas>
</div>
<div style="font-family:'fff'">&nbsp;</div> <!-- font preload -->

However, the first time I click on a player, the name on the card is centered using a default font size instead of the custom's one, that still is correctly loaded and shown:

This is the result at the first click with custom font loaded:

enter image description here

This is the result at further clicks with custom font loaded:

enter image description here

This is the result at the first click with default font. I commented out the ctx.font = "12px fff" line and the resulting text it's perfectly centered, so my assumption about the default font size used for the first measure is correct:

enter image description here

I think this has something to do with asynchronous loading of the font but I don't get why because the font is actually loaded and shown.

What am I missing?


Solution

  • What you are missing is the order of the operations matters.

    If you do:

    const nameWidth = ctx.measureText(player.name).width;
    ctx.font = "12px fff";
    

    you're setting the context to a different font after measuring the text. So it will return the width of whatever font the context has been set to previously or use the default. On subsequent calls to the drawPlayerCard() method the context's font will be set to 'fff' and thus return the correct width.

    So simply change the above to this:

    ctx.font = "12px fff";
    const nameWidth = ctx.measureText(player.name).width;