mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-16 19:25:38 +02:00
Implemented Google Fonts
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
}
|
||||
break;
|
||||
case ToolTypes.TEXT:
|
||||
if ( !layer.type === LAYER_TEXT ) {
|
||||
if ( layer.type !== LAYER_TEXT ) {
|
||||
this.setTool( null );
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -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 }) ];
|
||||
|
||||
@@ -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,
|
||||
|
||||
148
src/services/font-service.js
Normal file
148
src/services/font-service.js
Normal 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 );
|
||||
});
|
||||
};
|
||||
@@ -268,6 +268,7 @@ function handleKeyDown( event ) {
|
||||
commit( "addLayer", { type: LAYER_TEXT });
|
||||
}
|
||||
setActiveTool( ToolTypes.TEXT );
|
||||
preventDefault( event );
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -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 ) => {
|
||||
|
||||
@@ -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: {}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user