Rysowanie tekstu do <canvas> za pomocą @ font-face nie działa za pierwszym razem

97

Kiedy rysuję tekst na płótnie krojem, który jest ładowany za pomocą @ font-face, tekst nie jest wyświetlany poprawnie. W ogóle się nie wyświetla (w Chrome 13 i Firefox 5) lub krój pisma jest nieprawidłowy (Opera 11). Ten typ nieoczekiwanego zachowania występuje tylko na pierwszym rysunku z krojem pisma. Potem wszystko działa dobrze.

Czy to standardowe zachowanie czy coś?

Dziękuję Ci.

PS: Poniżej znajduje się kod źródłowy przypadku testowego

<!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>
lemonedo
źródło
1
Przeglądarki ładują czcionkę w tle, asynchronicznie. To normalne zachowanie. Zobacz także paulirish.com/2009/fighting-the-font-face-fout
Thomas

Odpowiedzi:

74

Rysowanie na płótnie musi nastąpić i powrócić natychmiast po wywołaniu fillTextmetody. Jednak przeglądarka nie załadowała jeszcze czcionki z sieci, co jest zadaniem w tle. Więc to musi spaść z powrotem do czcionki to nie masz dostępne.

Jeśli chcesz mieć pewność, że czcionka jest dostępna, załaduj ją wstępnie jakiś inny element na stronie, np .:

<div style="font-family: PressStart;">.</div>
bobince
źródło
3
Możesz ulec pokusie, aby dodać display: noneczcionkę, ale może to spowodować, że przeglądarki pomijają ładowanie czcionki. Lepiej jest użyć spacji zamiast ..
Thomas
6
Użycie spacji spowoduje, że IE wyrzuci węzeł białych znaków, który powinien znajdować się w elemencie div, nie pozostawiając tekstu do renderowania w czcionce. Oczywiście IE nie obsługuje jeszcze płótna, więc nie wiadomo, czy przyszłość IE będzie to robić i czy miałoby to wpływ na zachowanie ładowania czcionek, ale jest to długotrwały problem z analizowaniem HTML w IE.
bobince
1
Czy nie ma łatwiejszego sposobu na wstępne załadowanie czcionki? np. wymusić jakoś przez javascript?
Joshua
@Joshua: tylko tworząc element na stronie i ustawiając na nim czcionkę tj. tworzenie tych samych treści co powyżej, ale dynamicznie.
bobince
17
Dodanie tego nie gwarantuje, że czcionka zostanie już załadowana podczas wykonywania JavaScript. Musiałem wykonać skrypt, używając czcionki, o której mowa, z opóźnieniem (setTimeout), co oczywiście jest złe.
Nick
23

Użyj tej sztuczki i przypisz onerrorzdarzenie do Imageelementu.

Demo tutaj : działa na najnowszym 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 /programming/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);
};
płatny frajer
źródło
3
sprytna sztuczka. zwróć jednak uwagę, że ładujesz plik css, który z kolei zawiera odniesienie do rzeczywistego pliku czcionki (np. .ttf, .woff itp.). Musiałem użyć twojej sztuczki dwa razy, raz dla pliku css i raz dla pliku czcionki, do której się odwołuje (.woff), aby upewnić się, że wszystko zostało załadowane.
magma
1
Wypróbowałem to podejście z czcionką .ttf - nie działa stabilnie w przeglądarce Chrome (41.0.2272.101 m). Nawet setTimeout w 5 sekund nie pomaga - pierwsze renderowanie odbywa się z domyślną czcionką.
Michaił Fiadosenka
2
Powinieneś ustawić obsługę błędu onerror zanim ustawisz src
asdjfiasd
1
także "nowy obraz"; brak nawiasu.
asdjfiasd
@asdjfiasd Pareny nie są wymagane i mimo że srcatrybut jest ustawiony, ładowanie rozpocznie się w następnym bloku wykonania.
płatny kujon
16

Problem polega na tym, że próbujesz użyć czcionki, ale przeglądarka jeszcze jej nie załadowała i prawdopodobnie nawet o to nie poprosiła. Potrzebujesz czegoś, co załaduje czcionkę i da ci wywołanie zwrotne po załadowaniu; gdy otrzymasz oddzwonienie, wiesz, że możesz użyć czcionki.

Spójrz na Google WebFont Loader ; wygląda na to, że jest to „niestandardowy” dostawca i activewywołanie zwrotne po załadowaniu sprawi, że to zadziała.

Nigdy wcześniej go nie używałem, ale z szybkiego skanowania dokumentów potrzebnych do utworzenia pliku css fonts/pressstart2p.css, na przykład:

@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');
}

Następnie dodaj następujący 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);
  })();
ellisbben
źródło
14

Możesz załadować czcionki za pomocą FontFace API przed użyciem go w kanwie:

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

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

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

Zasób czcionki myfont.woff2jest najpierw pobierany. Po zakończeniu pobierania czcionka zostanie dodana do elementu FontFaceSet dokumentu .

Specyfikacja FontFace API jest roboczą wersją roboczą w momencie pisania tego tekstu. Zobacz tabelę zgodności przeglądarek tutaj.

Fred Bergman
źródło
1
Tęsknisz document.fonts.add. Zobacz odpowiedź Bruno.
Pacerier
Dzięki @Pacerier! Zaktualizowałem moją odpowiedź.
Fred Bergman
Ta technologia jest eksperymentalna i nie jest obsługiwana przez większość przeglądarek.
Michael Zelensky
11

A co z używaniem prostego kodu CSS do ukrycia elementu div za pomocą takiej czcionki:

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>
Gabriel Gonzalez
źródło
6

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');

});
Bruno Andrade
źródło
1

Nie jestem pewien, czy to ci pomoże, ale aby rozwiązać problem z moim kodem, po prostu utworzyłem pętlę for na górze mojego JavaScript, która przeszła przez wszystkie czcionki, które chciałem załadować. Następnie uruchomiłem funkcję, aby wyczyścić płótno i wstępnie załadować elementy, które chciałem, na płótnie. Jak dotąd działa idealnie. Taka była moja logika, którą zamieściłem poniżej:

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);
    }
grapien
źródło
Dodałem powyżej kod, aby zilustrować moją odpowiedź. Miałem podobny problem podczas tworzenia innej strony internetowej i to go rozwiązało, ponieważ po stronie serwera ładuje wszystkie czcionki, co pozwala im poprawnie wyświetlać się na stronie.
grapien
1

Napisałem jsfiddle zawierającego większość sugerowanych tutaj poprawek, ale żadna nie rozwiązała problemu. Jednak jestem początkującym programistą, więc być może nie zakodowałem poprawnie sugerowanych poprawek:

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);
}

Obejście problemu końcu poddałem się i przy pierwszym załadowaniu użyłem obrazu tekstu, jednocześnie umieszczając tekst z czcionkami poza obszarem wyświetlania płótna. Wszystkie kolejne wyświetlanie krojów czcionek w obszarze wyświetlania płótna działało bez problemu. W żadnym wypadku nie jest to eleganckie obejście.

Rozwiązanie jest wypiekane na mojej stronie, ale jeśli ktoś będzie potrzebował, spróbuję stworzyć jsfiddle, aby zademonstrować.

HatHead
źródło
1

Niektóre przeglądarki obsługują ten Ładowanie CSS czcionki specyfikacji. Pozwala na zarejestrowanie wywołania zwrotnego po załadowaniu wszystkich czcionek. Możesz opóźnić rysowanie płótna (lub przynajmniej rysowanie tekstu na płótnie) do tego czasu i wywołać przerysowanie, gdy czcionka będzie dostępna.

MvG
źródło
0

Przede wszystkim użyj narzędzia do ładowania czcionek internetowych Google, zgodnie z zaleceniami w drugiej odpowiedzi, i dodaj kod rysunku do wywołania zwrotnego, które zapewnia, aby wskazać, że czcionki zostały załadowane. Jednak to nie koniec historii. Od tego momentu jest to bardzo zależne od przeglądarki. W większości przypadków będzie działać dobrze, ale czasami może być konieczne odczekanie kilkuset milisekund lub użycie czcionek w innym miejscu na stronie. Wypróbowałem różne opcje, a jedyną metodą, która zawsze działa afaik, jest szybkie narysowanie niektórych wiadomości testowych na płótnie z kombinacjami rodziny czcionek i rozmiaru czcionki, których będziesz używać. Możesz to zrobić z tym samym kolorem co tło, więc nie będą one nawet widoczne i stanie się to bardzo szybko. Po tym czcionki zawsze działały dla mnie i we wszystkich przeglądarkach.

Megas
źródło
0

Moja odpowiedź dotyczy czcionek internetowych Google, a nie @ font-face. Wszędzie szukałem rozwiązania problemu z niewidoczną czcionką na płótnie. Próbowałem timerów, setInterval, bibliotek opóźnienia czcionek i wszelkiego rodzaju sztuczek. Nic nie działało. (W tym umieszczenie rodziny czcionek w CSS dla kanwy lub identyfikatora elementu canvas).

Jednak odkryłem, że animowanie tekstu renderowanego czcionką Google działa łatwo. Co za różnica? W animacji płótna wielokrotnie rysujemy animowane elementy. Więc wpadłem na pomysł, aby dwukrotnie wyrenderować tekst.

To też nie zadziałało - dopóki nie dodałem również krótkiego (100 ms) opóźnienia timera. Jak dotąd testowałem tylko na komputerze Mac. Chrome działał dobrze przy 100 ms. Safari wymagało przeładowania strony, więc zwiększyłem licznik czasu do 1000 i było dobrze. Firefox 18.0.2 i 20.0 nie załadowałby niczego na płótnie, gdybym używał czcionek Google (w tym wersji animacji).

Pełny kod: http://www.macloo.com/examples/canvas/canvas10.html

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

macloo
źródło
0

Napotyka ten sam problem. Po przeczytaniu „bobince” i innych komentarzy używam następującego javascript, aby to obejść:

$('body').append("<div id='loadfont' style='font-family: myfont;'>.</div>");
$('#loadfont').remove();
sglai
źródło
0

Jeśli chcesz przerysowywać za każdym razem, gdy ładowana jest nowa czcionka (i prawdopodobnie zmieniasz renderowanie), interfejs ładujący czcionkę ma również fajne zdarzenie . Miałem problemy z Promise w całkowicie dynamicznym środowisku.

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
}
HolgerJeromin
źródło
0

Kanwa jest rysowana niezależnie od ładowania DOM. Technika wstępnego ładowania będzie działać tylko wtedy, gdy płótno zostanie narysowane po wstępnym załadowaniu.

Moje rozwiązanie, nawet jeśli nie jest najlepsze:

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();
}
Eduardo Mihalache
źródło
-4

Dodaj opóźnienie, jak poniżej

<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>
cokolwiek
źródło