Implemented Google Fonts

This commit is contained in:
Igor Zinken
2020-12-30 11:24:11 +01:00
parent 9d3001ffbe
commit 72872fa8dc
10 changed files with 245 additions and 33 deletions

View File

@@ -56,7 +56,6 @@ npm run lint
# TODO / Roadmap
* Implement text editing using Google fonts!
* Try very wide Dropbox image on MB Air with space + pan in bottom right area. Won't work.
* Restoring positions of documents with positioned rotated objects is not accurate
* Issue with drawing mask on mirrored content

View File

@@ -21,14 +21,22 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
<template>
<div class="tool-option">
<div class="tool-option"
@focusin="handleFocus"
@focusout="handleBlur"
>
<h3 v-t="'text'"></h3>
<div class="wrapper input">
<textarea
ref="textInput"
v-model="text"
class="full"
@focus="handleTextFocus"
@blur="handleTextBlur"
/>
</div>
<div class="wrapper input">
<label v-t="'font'"></label>
<select-box :options="fonts"
v-model="font"
/>
</div>
<div class="wrapper input">
@@ -54,16 +62,24 @@
<script>
import { mapGetters, mapMutations } from "vuex";
import Slider from "@/components/ui/slider/slider";
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 messages from "./messages.json";
export default {
i18n: { messages },
components: {
SelectBox,
Slider,
},
data: () => ({
internalText: "",
renderPending: false,
}),
computed: {
...mapGetters([
"activeLayerIndex",
@@ -73,16 +89,28 @@ export default {
// load async as this adds to the bundle size
return () => import( "@/components/ui/color-picker/color-picker" );
},
fonts() {
return mapSelectOptions( [ ...googleFonts ].sort() );
},
text: {
get() {
return this.activeLayer.text?.value;
return this.internalText;
},
set( value ) {
this.updateLayer({
index: this.activeLayerIndex,
opts: { text: { value, size: this.size, font: "Arial", color: this.color } },
});
this.requestRender();
this.internalText = value;
// debounce the modle update (and subsequent text render)
// to not update on each entered character
if ( this.renderPending ) {
return;
}
this.renderPending = true;
window.setTimeout(() => {
this.renderPending = false;
this.updateLayer({
index: this.activeLayerIndex,
opts: { text: { value: this.text, size: this.size, font: this.font, color: this.color } },
});
}, 75 );
}
},
size: {
@@ -92,9 +120,8 @@ export default {
set( value ) {
this.updateLayer({
index: this.activeLayerIndex,
opts: { text: { value: this.text, size: value, font: "Arial", color: this.color } },
opts: { text: { value: this.text, size: value, font: this.font, color: this.color } },
});
this.requestRender();
}
},
color: {
@@ -104,28 +131,53 @@ export default {
set( value ) {
this.updateLayer({
index: this.activeLayerIndex,
opts: { text: { value: this.text, size: this.size, font: "Arial", color: value } },
opts: { text: { value: this.text, size: this.size, font: this.font, color: value } },
});
this.requestRender();
}
},
font: {
get() {
return this.activeLayer.text?.font;
},
async set( value ) {
const fromCache = await loadGoogleFont( value );
this.updateLayer({
index: this.activeLayerIndex,
opts: { text: { value: this.text, size: this.size, font: value, color: this.color } },
});
// on first load, font is not immediately available for rendering
if ( !fromCache ) {
window.setTimeout(() => {
getSpriteForLayer( this.activeLayer )?.cacheEffects();
}, 50 );
}
}
}
},
watch: {
activeLayer: {
immediate: true,
handler( layer ) {
this.internalText = layer.text?.value;
}
},
},
mounted() {
this.$refs.textInput?.focus();
},
destroyed() {
this.handleTextBlur();
this.handleBlur();
},
methods: {
...mapMutations([
"updateLayer",
]),
handleTextFocus() {
handleFocus() {
KeyboardService.setSuspended( true );
},
handleTextBlur() {
handleBlur() {
KeyboardService.setSuspended( false );
},
requestRender() {
getSpriteForLayer( this.activeLayer )?.cacheEffects();
},
},
};
</script>

View File

@@ -173,7 +173,7 @@ export default {
}
break;
case ToolTypes.TEXT:
if ( !layer.type === LAYER_TEXT ) {
if ( layer.type !== LAYER_TEXT ) {
this.setTool( null );
}
break;

View File

@@ -30,7 +30,7 @@ const DocumentFactory = {
* all layers and image content)
*/
create({
name = "New document", width = 400, height = 300, layers = [], selections = {}
name = "New document", width = 1000, height = 1000, layers = [], selections = {}
} = {}) {
if ( !layers.length ) {
layers = [ LayerFactory.create({ width, height }) ];

View File

@@ -20,8 +20,10 @@
* 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";
const TextFactory = {
create({ font = "Arial", value = "", size = 16, color = "red" } = {}) {
create({ font = googleFonts[ 0 ], value = "", size = 16, color = "red" } = {}) {
return {
font,
value,

View File

@@ -0,0 +1,148 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - 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.
*/
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)
*/
export const loadGoogleFont = fontName => {
return new Promise(( resolve, reject ) => {
if ( loadedFonts.has( fontName )) {
resolve( true );
return;
}
const css = document.createElement( "link" );
css.setAttribute( "rel", "stylesheet" );
css.setAttribute( "type", "text/css" );
css.onload = () => {
loadedFonts.add( fontName );
resolve( false );
};
css.onerror = e => {
console.error( `Could not load font ${fontName}`, e );
reject();
}
css.setAttribute( "href", `${GOOGLE_FONTS_URL}${fontName}` );
document.getElementsByTagName( "head" )[ 0 ].appendChild( css );
});
};

View File

@@ -268,6 +268,7 @@ function handleKeyDown( event ) {
commit( "addLayer", { type: LAYER_TEXT });
}
setActiveTool( ToolTypes.TEXT );
preventDefault( event );
}
break;

View File

@@ -26,6 +26,7 @@ import { LAYER_TEXT } from "@/definitions/layer-types";
import { getSpriteForLayer } from "@/factories/sprite-factory";
import { createCanvas, resizeToBase64 } from "@/utils/canvas-util";
import { getRotatedSize, getRotationCenter, getRectangleForSelection } from "@/utils/image-math";
import { loadGoogleFont } from "@/services/font-service";
const queue = [];
@@ -139,15 +140,23 @@ const hasEffects = ( layer ) => {
const renderText = async layer => {
const { text } = layer;
if ( !text.value ) {
return;
}
let font = text.font;
try {
await loadGoogleFont( font ); // lazily loads font file upon first request
} catch {
font = "Arial"; // fall back to universally available Arial
}
const sourceCtx = layer.source.getContext( "2d" );
sourceCtx.clearRect( 0, 0, layer.source.width, layer.source.height );
const lines = text.value.split( "\n" );
sourceCtx.font = `${text.size}px ${text.font}`;
sourceCtx.font = `${text.size}px ${font}`;
sourceCtx.fillStyle = text.color;
const lines = text.value.split( "\n" );
const lineHeight = text.size;
let y = 0;
lines.forEach(( line, index ) => {

View File

@@ -15,8 +15,8 @@ describe( "Document factory", () => {
expect( document ).toEqual({
id: expect.any( String ),
name: "New document",
width: 400,
height: 300,
width: 1000,
height: 1000,
layers: [ { layer: "1" } ],
selections: {}
});

View File

@@ -1,4 +1,5 @@
import TextFactory from "@/factories/text-factory";
import TextFactory from "@/factories/text-factory";
import { googleFonts } from "@/services/font-service";
describe( "Text factory", () => {
describe( "when creating a new Text instance", () => {
@@ -7,7 +8,7 @@ describe( "Text factory", () => {
expect( text ).toEqual({
size: expect.any( Number ),
value: "",
font: "Arial",
font: googleFonts[ 0 ],
color: "red",
});
});