mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Implement a little more robust Google Font preloading
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
118
src/definitions/font-types.js
Normal file
118
src/definitions/font-types.js
Normal 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",
|
||||
];
|
||||
@@ -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 ),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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" );
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" );
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user