[SOLVED] Can the font images be generated at runtime?

So, basically, I’m doing a game translated to 30+ languages. Playcanvas font system generates images for used characters and it raises the build size.

I don’t want to include all logographic languages (chinese, japanese etc.) in one build - it would be extremely heavy. So, how to do it more lightweight?

Two ideas I have are: 1. generate specific set of images from the font at runtime, 2. download the pregenerated images from my server and use them as a font.

But I have no idea if those ideas are even doable, and if they are - where do I start?

  1. Trickier as you need to find a font generator that can be executed in the browser.

  2. Should be do doable. You might be able to get away with unticking preload on the asset and only trigger the load of the asset when you know what language to use. The total build size will still be huge but the amount of data that the user will download will change. (example project here with a texture: https://developer.playcanvas.com/en/tutorials/loading-an-asset-at-runtime/)

  3. Use HTML for UI instead?

1 Like

#2. The platform I’m dealing with downloads a .zip with the game and serves it to the user only after the download is complete. Because of that I have to keep the build size as low as possible (right now the whole game is below 4MB).

Do I understand correctly, that this method would include: A. making a couple of font assets, each with different language characters configured, B. unticking preload for all of them, C. trigger asset load on runtime, choosing only one of those fonts.

#3. The game is nearly completed and it has 30+ UI windows, switching to HTML at this stage would be an overkill. (I assume we would have to make it from scratch)

If it has to be packaged as a zip (odd but hey) and you are permitted to have external assets, you can host the font assets on a server somewhere. There’s no example for fonts but a few for things like models and textures.

Not sure what the practical workflow would be though as it means you can’t have the fonts in the project when you build but you somehow still have to edit the UI in the editor.

OK, after some initial tests I can say I probably figured it out.

For anyone who gets to the same problem (and they should, as this is an important feature for everyone struggling with lightweight HTML5 games), here’s a rough explanation with some stripped code snippets:

Runtime pc.Font consist of two items: generated textures array and metadata (intensity, uvs for each registered character etc), so I had to download the textures as pngs and the metadata as json files. My workflow for exporting it to server is:

  • Prepare char lists for all the language you’re using. I store my translations on Google Spreadsheet and I’ve created a script to prepare all used characters for all used languages.
  • In playcanvas, duplicate the font and import it multiple times - if the base font is “Font”, then name the duplicates Font-en, Font-es, Font-zh etc. Paste appropriate character lists for each of them. Tick preload and put a “font” tag to the ones you want to export
  • Here’s a ts script to download all Font data (browser prompts you to download each png and each json file):
private processFonts () {
        let fonts = pc.app.assets.findByTag( "font" );
        let jsons = [];
        for ( let font of fonts ) {
            let r = font.resource as any;
            if ( !r ) {
                console.log( "Font " + font.name + " unloaded - omitting" );
                continue;
            }
            console.log( "Font " + font.name + " - gathered" );
            jsons.push( {
                fontName: r._data.info.face,
                textureUrls: r.textures.map( ( t ) => t.name ),
                data: r._data
            } );
        }
        console.log( "Jsons gathered!" );

        console.log( "Downloading textures" );
        for ( let json of jsons ) {
            let j = 0;
            for ( let i in json.textureUrls ) {
                let url = json.textureUrls[ i ];
                let newFilename = json.fontName + ( j++ ) + ".png";
                fetch( "https://launch.playcanvas.com" + url ).then( ( r ) => r.blob() ).then( ( blob ) => {
                    // @ts-ignore
                    let url2 = ( window.webkitURL || window.URL ).createObjectURL( blob );
                    let element = document.createElement( 'a' );
                    location.href = url;
                    element.setAttribute( 'href', url2 );
                    element.setAttribute( 'download', newFilename );
                    element.style.display = 'none';
                    document.body.appendChild( element );
                    element.click();
                    document.body.removeChild( element );
                } ).catch( ( e ) => { console.error( e ) } );
                json.textureUrls[ i ] = newFilename;
            }
        }


        for ( let json of jsons ) {
            let filename = json.fontName + ".json";
            let text = JSON.stringify( json );
            console.log( "Downloading " + filename );

            let element = document.createElement( 'a' );
            element.setAttribute( 'href', 'data:text/plain;charset=utf-8,' + encodeURIComponent( text ) );
            element.setAttribute( 'download', filename );
            element.style.display = 'none';
            document.body.appendChild( element );
            element.click();
            document.body.removeChild( element );

        }

    }
  • Then, I upload all files to an external server (both .jsons and .pngs)
  • In the game, I check the player language code and download a proper json file. It contains urls to all used textures, so I download all of them at once. When everything’s ready, I manually create a pc.Font asset.
export interface IServerFont {
        faceName: string;
        textureUrls: string[];
        data: any;
    }

    private download ( originalName: string ): Promise<pc.Font> {
        let newFontName: string = originalName + "-" + getLanguageCode();
        return fetch( getServerUrl() + newFontName + ".json" ).then( ( t ) => t.text() );
        } ).then( ( jsonText ) => {
            serverFont = JSON.parse( jsonText ) as IServerFont;
            let promises = []; // download all the textures at once
            for ( let texUrl of serverFont.textureUrls ) {
                promises.push( AssetsDownloader.get.download( "fonts/" + texUrl, "texture" ) ); // AssetsDownloader downloads a png and makes a pc.Asset out of it
            }
            return Promise.all( promises );
        } ).then( ( assets ) => {
            let textures = assets.map( ( a ) => a.resource );
            return new pc.Font( textures, serverFont.data );
        } );
    }
  • Remove all duplicate font files from the playcanvas project.

It’s not going to work out-of-the-box for everyone, but it should be enough for a starting point.

2 Likes