diff --git a/README.md b/README.md index 71fafce..2aebd52 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/components/options-panel/tool-options-text/tool-options-text.vue b/src/components/options-panel/tool-options-text/tool-options-text.vue index fe45b66..7c689a2 100644 --- a/src/components/options-panel/tool-options-text/tool-options-text.vue +++ b/src/components/options-panel/tool-options-text/tool-options-text.vue @@ -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 ); - } } } }, diff --git a/src/definitions/font-types.js b/src/definitions/font-types.js new file mode 100644 index 0000000..83cabcc --- /dev/null +++ b/src/definitions/font-types.js @@ -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", +]; diff --git a/src/factories/layer-factory.js b/src/factories/layer-factory.js index 61a2cfe..7b4ef36 100644 --- a/src/factories/layer-factory.js +++ b/src/factories/layer-factory.js @@ -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 ), }); } diff --git a/src/factories/text-factory.js b/src/factories/text-factory.js index 1d424e6..dc77332 100644 --- a/src/factories/text-factory.js +++ b/src/factories/text-factory.js @@ -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, diff --git a/src/services/font-service.js b/src/services/font-service.js index 94d0975..41f0e47 100644 --- a/src/services/font-service.js +++ b/src/services/font-service.js @@ -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 ); diff --git a/src/store/index.js b/src/store/index.js index abb25a7..bfd5a1a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 ) { diff --git a/tests/unit/factories/text-factory.spec.js b/tests/unit/factories/text-factory.spec.js index dec7ea7..80cabbd 100644 --- a/tests/unit/factories/text-factory.spec.js +++ b/tests/unit/factories/text-factory.spec.js @@ -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" ); }); }); }); diff --git a/tests/unit/store/store.spec.js b/tests/unit/store/store.spec.js index d1dd1ed..10700c9 100644 --- a/tests/unit/store/store.spec.js +++ b/tests/unit/store/store.spec.js @@ -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" ); }); });