I'm trying to display a custom web font on a <canvas>
element, however it doesn't always display correctly first time as the font has not been pre-loaded:
How it currently looks:
Here's how it should look:
I'm aware that several methods of pre-loading web fonts exist but unfortunately they're not applicable to this situation because the page can use up to 90 fonts.
The reason for the large number is because I'm using a Chinese font which has been segmented into many separate .woff2
files so that my CSS file can make use of the unicode-range
descriptor like so:
/* unicode range [1] */
@font-face {
font-family: 'ma-shan-zheng';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Ma Shan Zheng Regular'), local('MaShanZheng-Regular'), url(ma-shan-zheng.5.woff2) format('woff2');
unicode-range: U+fee3, U+fef3, U+ff03-ff04, U+ff07, U+ff0a, U+ff17-ff19, U+ff1c-ff1d, U+ff20-ff3a, U+ff3c, U+ff3e-ff5b, U+ff5d, U+ff61-ff65, U+ff67-ff6a, U+ff6c, U+ff6f-ff78, U+ff7a-ff7d, U+ff80-ff84, U+ff86, U+ff89-ff8e, U+ff92, U+ff97-ff9b, U+ff9d-ff9f, U+ffe0-ffe4, U+ffe6, U+ffe9, U+ffeb, U+ffed, U+fffc, U+1f004, U+1f170-1f171, U+1f192-1f195, U+1f198-1f19a, U+1f1e6-1f1e8;
}
/* unicode range [2] */
@font-face {
font-family: 'ma-shan-zheng';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Ma Shan Zheng Regular'), local('MaShanZheng-Regular'), url(ma-shan-zheng.6.woff2) format('woff2');
unicode-range: U+f0a7, U+f0b2, U+f0b7, U+f0c9, U+f0d8, U+f0da, U+f0dc-f0dd, U+f0e0, U+f0e6, U+f0eb, U+f0fc, U+f101, U+f104-f105, U+f107, U+f10b, U+f11b, U+f14b, U+f18a, U+f193, U+f1d6-f1d7, U+f244, U+f27a, U+f296, U+f2ae, U+f471, U+f4b3, U+f610-f611, U+f880-f881, U+f8ec, U+f8f5, U+f8ff, U+f901, U+f90a, U+f92c-f92d, U+f934, U+f937, U+f941, U+f965, U+f967, U+f969, U+f96b, U+f96f, U+f974, U+f978-f979, U+f97e, U+f981, U+f98a, U+f98e, U+f997, U+f99c, U+f9b2, U+f9b5, U+f9ba, U+f9be, U+f9ca, U+f9d0-f9d1, U+f9dd, U+f9e0-f9e1, U+f9e4, U+f9f7, U+fa00-fa01, U+fa08, U+fa0a, U+fa11, U+fb01-fb02, U+fdfc, U+fe0e, U+fe30-fe31, U+fe33-fe44, U+fe49-fe52, U+fe54-fe57, U+fe59-fe66, U+fe68-fe6b, U+fe8e, U+fe92-fe93, U+feae, U+feb8, U+fecb-fecc, U+fee0;
}
/* etc. all the way up to [90] */
This has the obvious benefit of only downloading the relevant .woff2
file when it's needed, but it also means that when the user downloads the web font for the first time they will see the unstyled text as shown above.
In an ideal world I'd be able to attach a callback function to the automatic download of the fonts, but it seems there is no access to this part of the browser's behaviour.
I've modified a solution from an old SO question about web font pre-loading - it's very hacky, but it does work.
In short, it creates a <span>
element with some text in a default font, measures the width/height, sets the element font to the web font, measures the size again and compares the results. If the size has changed, it's assumed the web font has loaded:
function waitFontLoaded(font, phrase, callback) {
var node = document.createElement("span");
// Set node content to the desired phrase/text
node.innerHTML = phrase;
// Visible - so we can measure it - but not on the screen
node.style.position = "absolute";
node.style.left = "-10000px";
node.style.top = "-10000px";
// Large font size makes even subtle changes obvious
node.style.fontSize = "300px";
// Reset any font properties
node.style.fontFamily = "sans-serif";
node.style.fontVariant = "normal";
node.style.fontStyle = "normal";
node.style.fontWeight = "normal";
node.style.letterSpacing = "0";
document.body.appendChild(node);
// Remember size with no applied web font
var width = node.offsetWidth;
var height = node.offsetHeight;
node.style.fontFamily = font + ", sans-serif";
var interval;
// Compare current size with original size
function checkFont() {
if (node && (node.offsetWidth !== width || node.offsetHeight !== height)) {
node.parentNode.removeChild(node);
node = null;
clearInterval(interval);
callback();
return true;
}
return false;
}
if (!checkFont()) {
interval = setInterval(checkFont, 50);
}
}
As I said, this does work, but is clearly not a robust solution as it's not impossible for two characters to be the same size in both the default system font and the web font.
Another very hacky solution would be to simply refresh the <canvas>
element every second, e.g. by using a setInterval
.
I feel like there must be a cleaner and more elegant way of doing this. Can anyone offer any suggestions?
In an ideal world I'd be able to attach a callback function to the automatic download of the fonts
I wouldn't call it an ideal world yet, but you can actually do that.
The document.fonts.ready
Promise resolves when all the fonts necessary to render visible text on the page have loaded.
Not far from there, you can iterate through document.fonts
which holds all the FontFaces that have been declared and check if they have loaded or not, along with their defined unicode-range if required.
document.fonts.ready.then( () => {
const loaded_fonts = [ ...document.fonts ]
// simplify the objects for logging here
.map( ({unicodeRange, status}, index) => ({ unicodeRange, status, index }) )
.filter( ({status}) => status === "loaded" );
console.log( loaded_fonts );
});
/* cyrillic-ext index:0 */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic index:1*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext index:2*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek index:3*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese index:4 - should be loaded */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext index:5 - should be loaded */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin index:6 - should be loaded */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
body {
font-family: "Roboto";
}
Hello thế giới
If you need to load a particular one before using it on a canvas, you can call document.fonts.load("your font", the_text_to_render)
which will load all the FontFaces required to render the_text_to_render
:
( async () => {
// <DEMO only>
// just to be sure the font was not loaded yet
await document.fonts.ready;
logLoadedFontsCount( "after document.fonts ready" );
// </DEMO only>
// now try to draw using that font face anyway
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const font_shorthand = "30px Roboto";
const text = "Привет мир";
// force loading fonts
await document.fonts.load( font_shorthand, text );
// now we can use it
ctx.font = font_shorthand;
ctx.fillText( text, 30, 50 );
// <DEMO only>
logLoadedFontsCount( "after loading of customs fonts" );
// </DEMO only>
} )();
// <DEMO only>
// logs how many FontFaces are currently loaded
function logLoadedFontsCount( when = "" ) {
const loaded_fonts = [ ...document.fonts ]
.filter( ({status}) => status === "loaded" );
console.log( "%s fonts loaded %s", loaded_fonts.length, when );
}
// </DEMO only>
/* cyrillic-ext index:0 */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic index:1*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext index:2*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek index:3*/
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese index:4 - should be loaded */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext index:5 - should be loaded */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin index:6 - should be loaded */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
<canvas></canvas>