ChatGPT解决这个技术问题 Extra ChatGPT

Drawing text to <canvas> with @font-face does not work at the first time

When I draw a text in a canvas with a typeface that is loaded via @font-face, the text doesn't show correctly. It doesn't show at all (in Chrome 13 and Firefox 5), or the typeface is wrong (Opera 11). This type of unexpected behavior occurs only at the first drawing with the typeface. After then everything works fine.

Is it the standard behavior or something?

Thank you.

PS: Following is the source code of the test case

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>@font-face and &lt;canvas&gt;</title>
        <style id="css">
@font-face {
    font-family: 'Press Start 2P';
    src: url('fonts/PressStart2P.ttf');
}
        </style>
        <style>
canvas, pre {
    border: 1px solid black;
    padding: 0 1em;
}
        </style>
    </head>
    <body>
        <h1>@font-face and &lt;canvas&gt;</h1>
        <p>
            Description: click the button several times, and you will see the problem.
            The first line won't show at all, or with a wrong typeface even if it does.
            <strong>If you have visited this page before, you may have to refresh (or reload) it.</strong>
        </p>
        <p>
            <button id="draw">#draw</button>
        </p>
        <p>
            <canvas width="250" height="250">
                Your browser does not support the CANVAS element.
                Try the latest Firefox, Google Chrome, Safari or Opera.
            </canvas>
        </p>
        <h2>@font-face</h2>
        <pre id="view-css"></pre>
        <h2>Script</h2>
        <pre id="view-script"></pre>
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
        <script id="script">
var x = 30,
    y = 10;

$('#draw').click(function () {
    var canvas = $('canvas')[0],
        ctx = canvas.getContext('2d');
    ctx.font = '12px "Press Start 2P"';
    ctx.fillStyle = '#000';
    ctx.fillText('Hello, world!', x, y += 20);
    ctx.fillRect(x - 20, y - 10, 10, 10);
});
        </script>
        <script>
$('#view-css').text($('#css').text());
$('#view-script').text($('#script').text());
        </script>
    </body>
</html>
Browsers load the font in the background, asynchronously. This is normal behaviour. See also paulirish.com/2009/fighting-the-font-face-fout

b
bobince

Drawing on canvas has to happen and return immediately when you call the fillText method. However, the browser has not yet loaded the font from the network, which is a background task. So it has to fall back to the font it does have available.

If you want to make sure the font is available, have some other element on the page preload it, eg.:

<div style="font-family: PressStart;">.</div>

You might be tempted to add display: none, but that might cause browsers to skip loading the font. It's better to use a space instead of a ..
Using a space will cause IE to throw away the whitespace node that should be in the div, leaving no text to render in the font. Of course IE doesn't support canvas yet, so it's unknown whether future-IE will continue to do this, and whether that would have an effect on font loading behaviour, but it's a long-standing IE HTML-parsing problem.
Is there no easier way to preload the font? e.g. force it through javascript somehow?
@Joshua: only by creating an element on the page and setting the font on it, ie. creating the same content as above but dynamically.
Adding this does not guarantee that the font will already be loaded when the JavaScript is executed. I had to execute my script using the font in question delayed (setTimeout) which, of course, is bad.
a
a paid nerd

Use this trick and bind an onerror event to an Image element.

Demo here: works on the latest Chrome.

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'http://fonts.googleapis.com/css?family=Vast+Shadow';
document.getElementsByTagName('head')[0].appendChild(link);

// Trick from https://stackoverflow.com/questions/2635814/
var image = new Image();
image.src = link.href;
image.onerror = function() {
    ctx.font = '50px "Vast Shadow"';
    ctx.textBaseline = 'top';
    ctx.fillText('Hello!', 20, 10);
};

smart trick. note though that you're loading the css which, in turn, contains a reference to the real font file (e.g., .ttf, .woff, etc). I had to use your trick twice, once for the css file, and once for the referenced font file (.woff) to make sure that everything was loaded.
Tried this approach with .ttf font - doesn't work stably on Chrome (41.0.2272.101 m). Even the setTimeout in 5 seconds doesn't help - first render goes with default font.
You should set onerror handler before you set src
also "new Image;" has missing parenthesis.
Could you do something like this with a local font?
F
Fred Bergman

You can load fonts with the FontFace API before using it in the canvas:

const myFont = new FontFace('My Font', 'url(https://myfont.woff2)');

myFont.load().then((font) => {
  document.fonts.add(font);

  console.log('Font loaded');
});

The font resource myfont.woff2 is first downloaded. Once the download completes, the font is added to the document's FontFaceSet.

The specification of the FontFace API is a working draft at the time of this writing. See browser compatibility table here.


You're missing document.fonts.add. See bruno's answer.
Thanks @Pacerier ! Updated my answer.
This technology is experimental and not supported by most of the browsers.
e
ellisbben

The nub of the problem is that you are trying to use the font but the browser has not loaded it yet and possibly has not even requested it. What you need is something that will load the font and give you a callback once it is loaded; once you get the callback, you know it is okay to use the font.

Look at Google's WebFont Loader; it seems like a "custom" provider and an active callback after the load would make it work.

I've never used it before, but from a quick scan of the docs you need to make a css file fonts/pressstart2p.css, like this:

@font-face {
  font-family: 'Press Start 2P';
  font-style: normal;
  font-weight: normal;
  src: local('Press Start 2P'), url('http://lemon-factory.net/reproduce/fonts/Press Start 2P.ttf') format('ttf');
}

Then add the following JS:

  WebFontConfig = {
    custom: { families: ['Press Start 2P'],
              urls: [ 'http://lemon-factory.net/reproduce/fonts/pressstart2p.css']},
    active: function() {
      /* code to execute once all font families are loaded */
      console.log(" I sure hope my font is loaded now. ");
    }
  };
  (function() {
    var wf = document.createElement('script');
    wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
        '://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
    wf.type = 'text/javascript';
    wf.async = 'true';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(wf, s);
  })();

G
Gabriel Gonzalez

What about using simple CSS to hide a div using the font like this:

CSS:

#preloadfont {
  font-family: YourFont;
  opacity:0;
  height:0;
  width:0;
  display:inline-block;
}

HTML:

<body>
   <div id="preloadfont">.</div>
   <canvas id="yourcanvas"></canvas>
   ...
</body>

B
Bruno Andrade

https://drafts.csswg.org/css-font-loading/

var myFont = new FontFace('My Font', 'url(https://myfont.woff2)');

myFont.load().then(function(font){

  // with canvas, if this is ommited won't work
  document.fonts.add(font);

  console.log('Font loaded');

});

great answer - thank you
P
Patrick H. Lauke

i've bumped into the issue when playing with it recently http://people.opera.com/patrickl/experiments/canvas/scroller/

worked around it by adding the font-family to canvas directly in the CSS, so you can just add

canvas { font-family: PressStart; }


Doesn't seem to work: Tested in Chrome 12 Edit: Refresh a few times for it to miss the font
G
Gav_at_HRSTS

This article sorted out my issues with lazy loaded fonts not being displayed.

How to load web fonts to avoid performance issues and speed up page loading

This helped me ...

    <link rel="preload" as="font" href="assets/fonts/Maki2/fontmaki2.css" rel="stylesheet" crossorigin="anonymous">

g
grapien

I am not sure if this will help you, but to solve the problem with my code I simply created a for loop at the top of my Javascript which ran through all the fonts that I wanted to load. I then ran a function to clear the canvas and preload the items I wanted on the canvas. So far it has worked perfectly. That was my logic I have posted my code below:

var fontLibrary = ["Acme","Aladin","Amarante","Belgrano","CantoraOne","Capriola","CevicheOne","Chango","ChelaOne","CherryCreamSoda",
"ConcertOne","Condiment","Damion","Devonshire","FugazOne","GermaniaOne","GorditasBold","GorditasRegular",
"KaushanScript","LeckerliOne","Lemon","LilitaOne","LuckiestGuy","Molle","MrDafoe","MrsSheppards",
"Norican","OriginalSurfer","OswaldBold","OswaldLight","OswaldRegular","Pacifico","Paprika","Playball",
"Quando","Ranchers","SansitaOne","SpicyRice","TitanOne","Yellowtail","Yesteryear"];

    for (var i=0; i < fontLibrary.length; i++) {
        context.fillText("Sample",250,50);
        context.font="34px " + fontLibrary[i];
    }

    changefontType();

    function changefontType() {
        selfonttype = $("#selfontype").val();
        inputtextgo1();
    }

    function inputtextgo1() {
        var y = 50;
        var lineHeight = 36;
        area1text = document.getElementById("bag1areatext").value;
        context.clearRect(0, 0, 500, 95)
        context.drawImage(section1backgroundimage, 0, 0);
        context.font="34px " + selfonttype;
        context.fillStyle = seltextcolor;
        context.fillText(area1text, 250, y);
    }

I have added some code above to illustrate my answer. I had a similar problem when developing another webpage and this solved it as on a the server end it loads all the fonts thus allows them to display correctly on the webpage.
H
HatHead

I wrote a jsfiddle incorporating most of the suggested fixes here but none resolved the issue. However, I am a novice programmer so perhaps did not code the suggested fixes correctly:

http://jsfiddle.net/HatHead/GcxQ9/23/

HTML:

<!-- you need to empty your browser cache and do a hard reload EVERYTIME to test this otherwise it will appear to working when, in fact, it isn't -->

<h1>Title Font</h1>

<p>Paragraph font...</p>
<canvas id="myCanvas" width="740" height="400"></canvas>

CSS:

@import url(http://fonts.googleapis.com/css?family=Architects+Daughter);
 @import url(http://fonts.googleapis.com/css?family=Rock+Salt);
 canvas {
    font-family:'Rock Salt', 'Architects Daughter'
}
.wf-loading p {
    font-family: serif
}
.wf-inactive p {
    font-family: serif
}
.wf-active p {
    font-family:'Architects Daughter', serif;
    font-size: 24px;
    font-weight: bold;
}
.wf-loading h1 {
    font-family: serif;
    font-weight: 400;
    font-size: 42px
}
.wf-inactive h1 {
    font-family: serif;
    font-weight: 400;
    font-size: 42px
}
.wf-active h1 {
    font-family:'Rock Salt', serif;
    font-weight: 400;
    font-size: 42px;
}

JS:

// do the Google Font Loader stuff....
WebFontConfig = {
    google: {
        families: ['Architects Daughter', 'Rock Salt']
    }
};
(function () {
    var wf = document.createElement('script');
    wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
        '://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
    wf.type = 'text/javascript';
    wf.async = 'true';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(wf, s);
})();

//play with the milliseconds delay to find the threshold - don't forget to empty your browser cache and do a hard reload!
setTimeout(WriteCanvasText, 0);

function WriteCanvasText() {
    // write some text to the canvas
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
    context.font = "normal" + " " + "normal" + " " + "bold" + " " + "42px" + " " + "Rock Salt";
    context.fillStyle = "#d50";
    context.fillText("Canvas Title", 5, 100);
    context.font = "normal" + " " + "normal" + " " + "bold" + " " + "24px" + " " + "Architects Daughter";
    context.fillText("Here is some text on the canvas...", 5, 180);
}

Workaround I eventually gave in and, on the first load, used an image of the text while also positioning the text with the font-faces outside of the canvas display area. All subsequent displays of the font-faces within the canvas display area worked no problem. This is not an elegant workaround by any means.

The solution is baked into my website but if anyone needs I will try to create a jsfiddle to demonstrate.


M
MvG

Some browsers support the CSS Font Loading specification. It allows you to register register a callback for when all the fonts have been loaded. You can delay drawing your canvas (or at least drawing text into your canvas) until then, and trigger a redraw once the font is available.


E
Eduardo Mihalache

The canvas is drawn independently of the DOM loading. The preload technique will only work if the canvas is drawn after the preload.

My solution, even if it is not the best:

CSS:

.preloadFont {
    font-family: 'Audiowide', Impact, Charcoal, sans-serif, cursive;
    font-size: 0;
    position: absolute;
    visibility: hidden;
}

HTML:

<body onload="init()">
  <div class="preloadFont">.</div>
  <canvas id="yourCanvas"></canvas>
</body>

JavaScript:

function init() {
    myCanvas.draw();
}

M
Megas

First of all use Google's Web Font loader as was advised in the other answer and add your drawing code to the callback it provides to indicate the fonts have been loaded. However this is not the end of the story. From this point it is very browser dependent. Most of the time it will work fine, but sometimes it may be required to wait for couple hundreds of milliseconds or use the fonts somewhere else on the page. I tried different options and the one method that afaik always works is to quickly draw some test messages in the canvas with the font family and font size combinations that you are going to use. You can do it with the same color as background, so they won't even be visible and it will happen very fast. After that fonts always worked for me and in all browsers.


m
macloo

My answer addresses Google Web fonts rather than @font-face. I searched everywhere for a solution to the problem of the font not showing up in the canvas. I tried timers, setInterval, font-delay libraries, and all kinds of tricks. Nothing worked. (Including putting font-family in the CSS for canvas or the ID of the canvas element.)

However, I found that animating text rendered in a Google font worked easily. What's the difference? In canvas animation, we re-draw the animated items again and again. So I got the idea to render the text twice.

That did not work either -- until I also added a short (100ms) timer delay. I've only tested on a Mac so far. Chrome worked fine at 100ms. Safari required a page reload, so I increased the timer to 1000, and then it was fine. Firefox 18.0.2 and 20.0 wouldn't load anything on the canvas if I was using Google fonts (including the animation version).

Full code: http://www.macloo.com/examples/canvas/canvas10.html

http://www.macloo.com/examples/canvas/scripts/canvas10.js


D
Davide Pastore

Faces same problem. After reading "bobince" and others comments, I uses following javascript to workaround it :

$('body').append("<div id='loadfont' style='font-family: myfont;'>.</div>");
$('#loadfont').remove();

H
HolgerJeromin

If you want to redraw everytime a new font is loaded (and probably change the rendering) the font loading api has a nice event for that, too. I had problems with the Promise in a complete dynamic environment.

var fontFaceSet = document.fonts;
if (fontFaceSet && fontFaceSet.addEventListener) {
    fontFaceSet.addEventListener('loadingdone', function () {
        // Redraw something

    });
} else {
    // no fallback is possible without this API as a font files download can be triggered
    // at any time when a new glyph is rendered on screen
}

J
JJJ

I try to use FontFaceSet.load to fix the problem: https://jsfiddle.net/wengshenshun/gr1zkvtq/30

const prepareFontLoad = (fontList) => Promise.all(fontList.map(font => document.fonts.load(font)))

You can find the browser compatibility from https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/load


Y
Yauheni Leichanok

Add a delay as below

<script>
var c = document.getElementById('myCanvas');    
var ctx = c.getContext('2d');
setTimeout(function() {
    ctx.font = "24px 'Proxy6'"; // uninstalled @fontface font style 
    ctx.textBaseline = 'top';
    ctx.fillText('What!', 20, 10);      
}, 100); 
</script>

关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now