Implement a little more robust Google Font preloading

This commit is contained in:
Igor Zinken
2021-01-05 08:53:08 +01:00
parent 05872365b2
commit 693ad055e7
9 changed files with 165 additions and 126 deletions

View File

@@ -68,7 +68,6 @@ npm run lint
* Drawing masks/brushes on a rotated layer that is panned or mirrored is broken
* Cloning contents onto a rotated layer is broken
* Render service should use the tool-options-text debounce on first font load to ensure font is present on document load
* Moving of flipped / rotated content leads to occasional non-renders (isInsideViewport doesn't take rotated rectangles into account)
* Restoring of document with rotated layers (smaller than document size) restores at incorrect offset

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -84,8 +84,8 @@ import SelectBox from '@/components/ui/select-box/select-box';
import Slider from "@/components/ui/slider/slider";
import { mapSelectOptions } from "@/utils/search-select-util";
import KeyboardService from "@/services/keyboard-service";
import { googleFonts, loadGoogleFont } from "@/services/font-service";
import { getSpriteForLayer } from "@/factories/sprite-factory";
import { loadGoogleFont } from "@/services/font-service";
import { googleFonts } from "@/definitions/font-types";
import messages from "./messages.json";
export default {
@@ -180,18 +180,11 @@ export default {
get() {
return this.activeLayer?.text?.font;
},
async set( font ) {
const fromCache = await loadGoogleFont( font );
set( font ) {
this.updateLayer({
index: this.activeLayerIndex,
opts: this.formatOpts({ font })
});
// on first load, font is not immediately available for rendering
if ( !fromCache ) {
window.setTimeout(() => {
getSpriteForLayer( this.activeLayer )?.cacheEffects();
}, 50 );
}
}
}
},

View File

@@ -0,0 +1,118 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export const googleFonts = [
// sans serif
"Open Sans", // listed first as it is a preferred neutral font
"Open Sans Condensed",
"Comfortaa",
"Noto Sans JP",
"Noto Sans KR",
"Noto Sans TC",
"Darker Grotesque",
"Gill Sans",
"Helvetica",
"Lato",
"Merriweather Sans",
"Montserrat",
"Mukta",
"Nanum Gothic",
"Nunito Sans",
"Poppins",
"Raleway",
"Roboto",
"Roboto Condensed",
"Roboto Slab",
"Rubik",
"Source Sans Pro",
"Ubuntu",
"Varela Round",
"Work Sans",
// monospace
"Anonymous Pro",
"B612 Mono",
"Courier Prime",
"Cutive Mono",
"Fira Code",
"Inconsolata",
"IBM Plex Mono",
"Jetbrains Mono",
"Major Mono Display",
"Nanum Gothic Coding",
"Nova Mono",
"Overpass Mono",
"Oxygen Mono",
"PT Mono",
"Roboto Mono",
"Source Code Pro",
"Space Mono",
"Ubuntu Mono",
"VT323",
"Xanh Mono",
// serif
"Arvo",
"Patua One",
"Playfair Display",
"Playfair Display SC",
"PT Serif",
"Merriweather",
"Lora",
"Noto Serif",
"Nunito",
// display
"Audiowide",
"Bangers",
"Bebas Neue",
"Bungee Outline",
"Hanalei Fill",
// handwriting / script
"Amatic SC",
"Architects Daughter",
"Caveat",
"Caveat Brush",
"Courgette",
"Dancing Script",
"Damion",
"Euphoria Script",
"Great Vibes",
"Indie Flower",
"Kaushan Script",
"Mali",
"Nanum Pen Script",
"Pacifico",
"Patrick Hand",
"Permanent Marker",
"Redressed",
"Rock Salt",
"Sacramento",
"Satisfy",
"Shadows Into Light",
"Yellowtail",
// fat glyphs (any of above styles)
"Abril Fatface",
"Alfa Slab One",
"Fredoka One",
"Staatliches",
"Oswald",
"Nerko One",
"Righteous",
];

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -88,6 +88,7 @@ const LayerFactory = {
async deserialize( layer ) {
const source = await base64ToLayerImage( layer.s, layer.t, layer.w, layer.h );
const mask = await base64ToLayerImage( layer.m, LAYER_MASK, layer.w, layer.h );
const text = await TextFactory.deserialize( layer.tx );
return LayerFactory.create({
name: layer.n,
type: layer.t,
@@ -101,7 +102,7 @@ const LayerFactory = {
width: layer.w,
height: layer.h,
visible: layer.v,
text: TextFactory.deserialize( layer.tx ),
text,
effects: EffectsFactory.deserialize( layer.f ),
});
}

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -20,7 +20,8 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { googleFonts } from "@/services/font-service";
import { loadGoogleFont } from "@/services/font-service";
import { googleFonts } from "@/definitions/font-types";
const TextFactory = {
create({
@@ -60,9 +61,11 @@ const TextFactory = {
* Creating a new text instance from a stored text structure
* inside a stored projects layer
*/
deserialize( text = {} ) {
async deserialize( text = {} ) {
const font = text.f;
await loadGoogleFont( font ); // ensure font is loaded and ready
return TextFactory.create({
font: text.f,
font,
value: text.v,
size: text.s,
lineHeight: text.l,

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -20,110 +20,15 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { createCanvas } from "@/utils/canvas-util";
const loadedFonts = new Set();
const GOOGLE_FONTS_URL = "https://fonts.googleapis.com/css?family=";
export const googleFonts = [
// sans serif
"Open Sans", // listed first as it is a preferred neutral font
"Open Sans Condensed",
"Comfortaa",
"Noto Sans JP",
"Noto Sans KR",
"Noto Sans TC",
"Darker Grotesque",
"Gill Sans",
"Helvetica",
"Lato",
"Merriweather Sans",
"Montserrat",
"Mukta",
"Nanum Gothic",
"Nunito Sans",
"Poppins",
"Raleway",
"Roboto",
"Roboto Condensed",
"Roboto Slab",
"Rubik",
"Source Sans Pro",
"Ubuntu",
"Varela Round",
"Work Sans",
// monospace
"Anonymous Pro",
"B612 Mono",
"Courier Prime",
"Cutive Mono",
"Fira Code",
"Inconsolata",
"IBM Plex Mono",
"Jetbrains Mono",
"Major Mono Display",
"Nanum Gothic Coding",
"Nova Mono",
"Overpass Mono",
"Oxygen Mono",
"PT Mono",
"Roboto Mono",
"Source Code Pro",
"Space Mono",
"Ubuntu Mono",
"VT323",
"Xanh Mono",
// serif
"Arvo",
"Patua One",
"Playfair Display",
"Playfair Display SC",
"PT Serif",
"Merriweather",
"Lora",
"Noto Serif",
"Nunito",
// display
"Audiowide",
"Bangers",
"Bebas Neue",
"Bungee Outline",
"Hanalei Fill",
// handwriting / script
"Amatic SC",
"Architects Daughter",
"Caveat",
"Caveat Brush",
"Courgette",
"Dancing Script",
"Damion",
"Euphoria Script",
"Great Vibes",
"Indie Flower",
"Kaushan Script",
"Mali",
"Nanum Pen Script",
"Pacifico",
"Patrick Hand",
"Permanent Marker",
"Redressed",
"Rock Salt",
"Sacramento",
"Satisfy",
"Shadows Into Light",
"Yellowtail",
// fat glyphs (any of above styles)
"Abril Fatface",
"Alfa Slab One",
"Fredoka One",
"Staatliches",
"Oswald",
"Nerko One",
"Righteous",
];
/**
* Lazily loads a Google font (defined in the list above)
* Returns boolean true indicating whether font was cache
* or false when it has just been loaded (and adde to the cache)
* or false when it has just been loaded (and added to the cache)
*/
export const loadGoogleFont = fontName => {
return new Promise(( resolve, reject ) => {
@@ -136,7 +41,14 @@ export const loadGoogleFont = fontName => {
css.setAttribute( "type", "text/css" );
css.onload = () => {
loadedFonts.add( fontName );
resolve( false );
// CSS file has loaded, but font hasn't, create first request for font render
const { ctx } = createCanvas();
ctx.font = `16px ${fontName}`;
ctx.fillText( "foo", 0, 0 );
// the above will have requested the font file, resolve Promise after slight delay
window.setTimeout(() => {
resolve( false );
}, 250 );
};
css.onerror = e => {
console.error( `Could not load font ${fontName}`, e );

View File

@@ -150,6 +150,7 @@ export default {
}
file = fileList[ 0 ];
}
commit( "setLoading", "doc" );
try {
const document = await DocumentFactory.fromBlob( file );
commit( "addNewDocument", document );
@@ -162,6 +163,7 @@ export default {
message: translate( "errorLoadingFile", { file: truncate( file.name, 35 ) })
});
}
commit( "unsetLoading", "doc" );
},
saveDocument({ commit, getters }, name = null ) {
if ( !name ) {

View File

@@ -1,5 +1,10 @@
import TextFactory from "@/factories/text-factory";
import { googleFonts } from "@/services/font-service";
import TextFactory from "@/factories/text-factory";
import { googleFonts } from "@/definitions/font-types";
let mockUpdateFn;
jest.mock( "@/services/font-service", () => ({
loadGoogleFont: (...args) => mockUpdateFn?.( "loadGoogleFont", ...args ),
}));
describe( "Text factory", () => {
describe( "when creating a new Text instance", () => {
@@ -37,6 +42,7 @@ describe( "Text factory", () => {
describe( "when serializing and deserializing a Text structure", () => {
it( "should do so without data loss", async () => {
mockUpdateFn = jest.fn();
const text = TextFactory.create({
size: 10,
lineHeight: 40,
@@ -45,10 +51,11 @@ describe( "Text factory", () => {
value: "Foo bar baz",
color: "#FFF"
});
const serialized = TextFactory.serialize( text );
const deserialized = TextFactory.deserialize( serialized );
const serialized = TextFactory.serialize( text );
const deserialized = await TextFactory.deserialize( serialized );
expect( deserialized ).toEqual( text );
expect( mockUpdateFn ).toHaveBeenCalledWith( "loadGoogleFont", "Helvetica" );
});
});
});

View File

@@ -214,8 +214,10 @@ describe( "Vuex store", () => {
// assert selected file is converted from Blob to document
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 2, "fromBlob", mockFile );
// assert resulting Document has been added as the active document
expect( commit ).toHaveBeenNthCalledWith( 1, "addNewDocument", mockDocument );
expect( commit ).toHaveBeenNthCalledWith( 2, "showNotification", expect.any( Object ));
expect( commit ).toHaveBeenNthCalledWith( 1, "setLoading", "doc" );
expect( commit ).toHaveBeenNthCalledWith( 2, "addNewDocument", mockDocument );
expect( commit ).toHaveBeenNthCalledWith( 3, "showNotification", expect.any( Object ));
expect( commit ).toHaveBeenNthCalledWith( 4, "unsetLoading", "doc" );
});
it( "should be able to load a saved document from a given File/Blob", async () => {
@@ -234,8 +236,10 @@ describe( "Vuex store", () => {
// assert give file is converted from Blob to document
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "fromBlob", blob );
// assert resulting Document has been added as the active document
expect( commit ).toHaveBeenNthCalledWith( 1, "addNewDocument", mockDocument );
expect( commit ).toHaveBeenNthCalledWith( 2, "showNotification", expect.any( Object ));
expect( commit ).toHaveBeenNthCalledWith( 1, "setLoading", "doc" );
expect( commit ).toHaveBeenNthCalledWith( 2, "addNewDocument", mockDocument );
expect( commit ).toHaveBeenNthCalledWith( 3, "showNotification", expect.any( Object ));
expect( commit ).toHaveBeenNthCalledWith( 4, "unsetLoading", "doc" );
});
});