From 63c10743fbbe075cc3171c2b9caaae6e6be8c3b7 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 27 May 2020 15:14:50 +0200 Subject: [PATCH] split font into fontSize and fontFamily (#1635) --- src/actions/actionProperties.tsx | 63 +++++---- src/actions/actionStyles.ts | 9 +- src/appState.ts | 7 +- src/components/App.tsx | 9 +- src/constants.ts | 7 + src/data/json.ts | 2 +- src/data/restore.ts | 32 ++++- src/element/newElement.test.ts | 3 +- src/element/newElement.ts | 11 +- src/element/textElement.ts | 4 +- src/element/textWysiwyg.tsx | 13 +- src/element/types.ts | 7 +- src/renderer/renderElement.ts | 15 +- src/scene/export.ts | 14 +- .../regressionTests.test.tsx.snap | 132 ++++++++++++------ src/types.ts | 4 +- src/utils.ts | 24 +++- 17 files changed, 240 insertions(+), 116 deletions(-) diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index 697d5691..e56ffc10 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -3,6 +3,7 @@ import { ExcalidrawElement, ExcalidrawTextElement, TextAlign, + FontFamily, } from "../element/types"; import { getCommonAttributeOfSelectedElements, @@ -17,9 +18,9 @@ import { import { ColorPicker } from "../components/ColorPicker"; import { AppState } from "../../src/types"; import { t } from "../i18n"; -import { DEFAULT_FONT } from "../appState"; import { register } from "./register"; import { newElementWith } from "../element/mutateElement"; +import { DEFAULT_FONT_SIZE, DEFAULT_FONT_FAMILY } from "../appState"; const changeProperty = ( elements: readonly ExcalidrawElement[], @@ -318,7 +319,7 @@ export const actionChangeFontSize = register({ elements: changeProperty(elements, appState, (el) => { if (isTextElement(el)) { const element: ExcalidrawTextElement = newElementWith(el, { - font: `${value}px ${el.font.split("px ")[1]}`, + fontSize: value, }); redrawTextBoundingBox(element); return element; @@ -328,9 +329,7 @@ export const actionChangeFontSize = register({ }), appState: { ...appState, - currentItemFont: `${value}px ${ - appState.currentItemFont.split("px ")[1] - }`, + currentItemFontSize: value, }, commitToHistory: true, }; @@ -349,8 +348,8 @@ export const actionChangeFontSize = register({ value={getFormValue( elements, appState, - (element) => isTextElement(element) && +element.font.split("px ")[0], - +(appState.currentItemFont || DEFAULT_FONT).split("px ")[0], + (element) => isTextElement(element) && element.fontSize, + appState.currentItemFontSize || DEFAULT_FONT_SIZE, )} onChange={(value) => updateData(value)} /> @@ -365,7 +364,7 @@ export const actionChangeFontFamily = register({ elements: changeProperty(elements, appState, (el) => { if (isTextElement(el)) { const element: ExcalidrawTextElement = newElementWith(el, { - font: `${el.font.split("px ")[0]}px ${value}`, + fontFamily: value, }); redrawTextBoundingBox(element); return element; @@ -375,33 +374,35 @@ export const actionChangeFontFamily = register({ }), appState: { ...appState, - currentItemFont: `${ - appState.currentItemFont.split("px ")[0] - }px ${value}`, + currentItemFontFamily: appState.currentItemFontFamily, }, commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => ( -
- {t("labels.fontFamily")} - isTextElement(element) && element.font.split("px ")[1], - (appState.currentItemFont || DEFAULT_FONT).split("px ")[1], - )} - onChange={(value) => updateData(value)} - /> -
- ), + PanelComponent: ({ elements, appState, updateData }) => { + const options: { value: FontFamily; text: string }[] = [ + { value: 1, text: t("labels.handDrawn") }, + { value: 2, text: t("labels.normal") }, + { value: 3, text: t("labels.code") }, + ]; + + return ( +
+ {t("labels.fontFamily")} + + group="font-family" + options={options} + value={getFormValue( + elements, + appState, + (element) => isTextElement(element) && element.fontFamily, + appState.currentItemFontFamily || DEFAULT_FONT_FAMILY, + )} + onChange={(value) => updateData(value)} + /> +
+ ); + }, }); export const actionChangeTextAlign = register({ diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index 9d6cbbe7..17f3758f 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -4,7 +4,11 @@ import { redrawTextBoundingBox, } from "../element"; import { KEYS } from "../keys"; -import { DEFAULT_FONT, DEFAULT_TEXT_ALIGN } from "../appState"; +import { + DEFAULT_FONT_SIZE, + DEFAULT_FONT_FAMILY, + DEFAULT_TEXT_ALIGN, +} from "../appState"; import { register } from "./register"; import { mutateElement, newElementWith } from "../element/mutateElement"; @@ -47,7 +51,8 @@ export const actionPasteStyles = register({ }); if (isTextElement(newElement)) { mutateElement(newElement, { - font: pastedElement?.font || DEFAULT_FONT, + fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE, + fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY, textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN, }); redrawTextBoundingBox(newElement); diff --git a/src/appState.ts b/src/appState.ts index d8a9c26e..92e21111 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -2,8 +2,10 @@ import oc from "open-color"; import { AppState, FlooredNumber } from "./types"; import { getDateTime } from "./utils"; import { t } from "./i18n"; +import { FontFamily } from "./element/types"; -export const DEFAULT_FONT = "20px Virgil"; +export const DEFAULT_FONT_SIZE = 20; +export const DEFAULT_FONT_FAMILY: FontFamily = 1; export const DEFAULT_TEXT_ALIGN = "left"; export const getDefaultAppState = (): AppState => { @@ -25,7 +27,8 @@ export const getDefaultAppState = (): AppState => { currentItemStrokeStyle: "solid", currentItemRoughness: 1, currentItemOpacity: 100, - currentItemFont: DEFAULT_FONT, + currentItemFontSize: DEFAULT_FONT_SIZE, + currentItemFontFamily: DEFAULT_FONT_FAMILY, currentItemTextAlign: DEFAULT_TEXT_ALIGN, viewBackgroundColor: oc.white, scrollX: 0 as FlooredNumber, diff --git a/src/components/App.tsx b/src/components/App.tsx index 7d50315f..db5cc52c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -751,7 +751,8 @@ class App extends React.Component { roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, text: text, - font: this.state.currentItemFont, + fontSize: this.state.currentItemFontSize, + fontFamily: this.state.currentItemFontFamily, textAlign: this.state.currentItemTextAlign, }); @@ -1319,7 +1320,8 @@ class App extends React.Component { initText: element.text, strokeColor: element.strokeColor, opacity: element.opacity, - font: element.font, + fontSize: element.fontSize, + fontFamily: element.fontFamily, angle: element.angle, textAlign: element.textAlign, zoom: this.state.zoom, @@ -1399,7 +1401,8 @@ class App extends React.Component { roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, text: "", - font: this.state.currentItemFont, + fontSize: this.state.currentItemFontSize, + fontFamily: this.state.currentItemFontFamily, textAlign: this.state.currentItemTextAlign, }); diff --git a/src/constants.ts b/src/constants.ts index 5e84160f..1e1c33b1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -58,3 +58,10 @@ export const BROADCAST = { export const CLASSES = { SHAPE_ACTIONS_MENU: "App-menu__left", }; + +// 1-based in case we ever do `if(element.fontFamily)` +export const FONT_FAMILY = { + 1: "Virgil", + 2: "Helvetica", + 3: "Cascadia", +} as const; diff --git a/src/data/json.ts b/src/data/json.ts index f005c043..8ac8608b 100644 --- a/src/data/json.ts +++ b/src/data/json.ts @@ -12,7 +12,7 @@ export const serializeAsJSON = ( JSON.stringify( { type: "excalidraw", - version: 1, + version: 2, source: window.location.origin, elements: elements.filter((element) => !element.isDeleted), appState: cleanAppStateForExport(appState), diff --git a/src/data/restore.ts b/src/data/restore.ts index afd98906..96bf71e8 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -1,6 +1,10 @@ import { Point } from "../types"; -import { ExcalidrawElement } from "../element/types"; +import { + ExcalidrawElement, + ExcalidrawTextElement, + FontFamily, +} from "../element/types"; import { AppState } from "../types"; import { DataState } from "./types"; import { @@ -10,7 +14,17 @@ import { } from "../element"; import { calculateScrollCenter } from "../scene"; import { randomId } from "../random"; -import { DEFAULT_TEXT_ALIGN } from "../appState"; +import { DEFAULT_TEXT_ALIGN, DEFAULT_FONT_FAMILY } from "../appState"; +import { FONT_FAMILY } from "../constants"; + +const getFontFamilyByName = (fontFamilyName: string): FontFamily => { + for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) { + if (fontFamilyString.includes(fontFamilyName)) { + return parseInt(id) as FontFamily; + } + } + return DEFAULT_FONT_FAMILY; +}; export const restore = ( // we're making the elements mutable for this API because we want to @@ -57,6 +71,20 @@ export const restore = ( element.points = points; } else { if (isTextElement(element)) { + if ("font" in element) { + const [fontPx, fontFamily]: [ + string, + string, + ] = (element as any).font.split(" "); + (element as Mutable).fontSize = parseInt( + fontPx, + 10, + ); + (element as Mutable< + ExcalidrawTextElement + >).fontFamily = getFontFamilyByName(fontFamily); + delete (element as any).font; + } if (!element.textAlign) { element.textAlign = DEFAULT_TEXT_ALIGN; } diff --git a/src/element/newElement.test.ts b/src/element/newElement.test.ts index 1199c5df..a7de6066 100644 --- a/src/element/newElement.test.ts +++ b/src/element/newElement.test.ts @@ -78,7 +78,8 @@ it("clones text element", () => { roughness: 1, opacity: 100, text: "hello", - font: "Arial 20px", + fontSize: 20, + fontFamily: 1, textAlign: "left", }); diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 22565068..68da072d 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -5,9 +5,10 @@ import { ExcalidrawGenericElement, NonDeleted, TextAlign, + FontFamily, GroupId, } from "../element/types"; -import { measureText } from "../utils"; +import { measureText, getFontString } from "../utils"; import { randomInteger, randomId } from "../random"; import { newElementWith } from "./mutateElement"; import nanoid from "nanoid"; @@ -77,16 +78,18 @@ export const newElement = ( export const newTextElement = ( opts: { text: string; - font: string; + fontSize: number; + fontFamily: FontFamily; textAlign: TextAlign; } & ElementConstructorOpts, ): NonDeleted => { - const metrics = measureText(opts.text, opts.font); + const metrics = measureText(opts.text, getFontString(opts)); const textElement = newElementWith( { ..._newElementBase("text", opts), text: opts.text, - font: opts.font, + fontSize: opts.fontSize, + fontFamily: opts.fontFamily, textAlign: opts.textAlign, // Center the text x: opts.x - metrics.width / 2, diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 394faee6..7a92e7c3 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -1,9 +1,9 @@ -import { measureText } from "../utils"; +import { measureText, getFontString } from "../utils"; import { ExcalidrawTextElement } from "./types"; import { mutateElement } from "./mutateElement"; export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => { - const metrics = measureText(element.text, element.font); + const metrics = measureText(element.text, getFontString(element)); mutateElement(element, { width: metrics.width, height: metrics.height, diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 3942bf00..7454c698 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -1,8 +1,9 @@ import { KEYS } from "../keys"; -import { selectNode, isWritableElement } from "../utils"; +import { selectNode, isWritableElement, getFontString } from "../utils"; import { globalSceneState } from "../scene"; import { isTextElement } from "./typeChecks"; import { CLASSES } from "../constants"; +import { FontFamily } from "./types"; const trimText = (text: string) => { // whitespace only → trim all because we'd end up inserting invisible element @@ -21,7 +22,8 @@ type TextWysiwygParams = { x: number; y: number; strokeColor: string; - font: string; + fontSize: number; + fontFamily: FontFamily; opacity: number; zoom: number; angle: number; @@ -37,7 +39,8 @@ export const textWysiwyg = ({ x, y, strokeColor, - font, + fontSize, + fontFamily, opacity, zoom, angle, @@ -68,7 +71,7 @@ export const textWysiwyg = ({ transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`, textAlign: textAlign, display: "inline-block", - font: font, + font: getFontString({ fontSize, fontFamily }), padding: "4px", // This needs to have "1px solid" otherwise the carret doesn't show up // the first time on Safari and Chrome! @@ -193,7 +196,7 @@ export const textWysiwyg = ({ .find((element) => element.id === id); if (editingElement && isTextElement(editingElement)) { Object.assign(editable.style, { - font: editingElement.font, + font: getFontString(editingElement), textAlign: editingElement.textAlign, color: editingElement.strokeColor, opacity: editingElement.opacity / 100, diff --git a/src/element/types.ts b/src/element/types.ts index 08a42418..d4567e50 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -1,4 +1,5 @@ import { Point } from "../types"; +import { FONT_FAMILY } from "../constants"; export type GroupId = string; @@ -49,7 +50,8 @@ export type NonDeletedExcalidrawElement = NonDeleted; export type ExcalidrawTextElement = _ExcalidrawElementBase & Readonly<{ type: "text"; - font: string; + fontSize: number; + fontFamily: FontFamily; text: string; baseline: number; textAlign: TextAlign; @@ -65,3 +67,6 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase & export type PointerType = "mouse" | "pen" | "touch"; export type TextAlign = "left" | "center" | "right"; + +export type FontFamily = keyof typeof FONT_FAMILY; +export type FontString = string & { _brand: "fontString" }; diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 18339fb1..a965900d 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -14,7 +14,7 @@ import { Drawable, Options } from "roughjs/bin/core"; import { RoughSVG } from "roughjs/bin/svg"; import { RoughGenerator } from "roughjs/bin/generator"; import { SceneState } from "../scene/types"; -import { SVG_NS, distance } from "../utils"; +import { SVG_NS, distance, getFontString, getFontFamilyString } from "../utils"; import { isPathALoop } from "../math"; import rough from "roughjs/bin/rough"; @@ -101,7 +101,7 @@ const drawElementOnCanvas = ( default: { if (isTextElement(element)) { const font = context.font; - context.font = element.font; + context.font = getFontString(element); const fillStyle = context.fillStyle; context.fillStyle = element.strokeColor; const textAlign = context.textAlign; @@ -492,13 +492,6 @@ export const renderElementToSvg = ( : element.textAlign === "right" ? element.width : 0; - const fontSplit = element.font.split(" ").filter((d) => !!d.trim()); - let fontFamily = fontSplit[0]; - let fontSize = "20px"; - if (fontSplit.length > 1) { - fontFamily = fontSplit[1]; - fontSize = fontSplit[0]; - } const textAnchor = element.textAlign === "center" ? "middle" @@ -510,8 +503,8 @@ export const renderElementToSvg = ( text.textContent = lines[i]; text.setAttribute("x", `${horizontalOffset}`); text.setAttribute("y", `${(i + 1) * lineHeight - verticalOffset}`); - text.setAttribute("font-family", fontFamily); - text.setAttribute("font-size", fontSize); + text.setAttribute("font-family", getFontFamilyString(element)); + text.setAttribute("font-size", `${element.fontSize}px`); text.setAttribute("fill", element.strokeColor); text.setAttribute("text-anchor", textAnchor); text.setAttribute("style", "white-space: pre;"); diff --git a/src/scene/export.ts b/src/scene/export.ts index 4facda45..3caa6bcc 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -4,10 +4,11 @@ import { newTextElement } from "../element"; import { NonDeletedExcalidrawElement } from "../element/types"; import { getCommonBounds } from "../element/bounds"; import { renderScene, renderSceneToSvg } from "../renderer/renderScene"; -import { distance, SVG_NS, measureText } from "../utils"; +import { distance, SVG_NS, measureText, getFontString } from "../utils"; import { normalizeScroll } from "./scroll"; import { AppState } from "../types"; import { t } from "../i18n"; +import { DEFAULT_FONT_FAMILY } from "../appState"; export const SVG_EXPORT_TAG = ``; @@ -149,12 +150,17 @@ export const exportToSvg = ( const getWatermarkElement = (maxX: number, maxY: number) => { const text = t("labels.madeWithExcalidraw"); - const font = "16px Virgil"; - const { width: textWidth } = measureText(text, font); + const fontSize = 16; + const fontFamily = DEFAULT_FONT_FAMILY; + const { width: textWidth } = measureText( + text, + getFontString({ fontSize, fontFamily }), + ); return newTextElement({ text, - font, + fontSize, + fontFamily, textAlign: "center", x: maxX - textWidth / 2, y: maxY + 16, diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 7a3515fd..2d4b68c8 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -5,7 +5,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -202,7 +203,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -320,7 +322,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "#fa5252", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#5f3dc4", @@ -566,7 +569,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -718,7 +722,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -905,7 +910,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -1101,7 +1107,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -1396,7 +1403,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -2216,7 +2224,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -2334,7 +2343,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -2452,7 +2462,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -2570,7 +2581,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -2710,7 +2722,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -2850,7 +2863,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -2990,7 +3004,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -3130,7 +3145,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -3248,7 +3264,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -3366,7 +3383,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -3506,7 +3524,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -3624,7 +3643,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -3764,7 +3784,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -3960,7 +3981,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -4021,7 +4043,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -4803,7 +4826,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -5180,7 +5204,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -5478,7 +5503,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -5701,7 +5727,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -5853,7 +5880,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -6586,7 +6614,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -7224,7 +7253,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -7771,7 +7801,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -8231,7 +8262,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -8649,7 +8681,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -8986,7 +9019,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -9246,7 +9280,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -9433,7 +9468,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -10215,7 +10251,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -10900,7 +10937,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -11492,7 +11530,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -11995,7 +12034,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -12251,7 +12291,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -12310,7 +12351,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -12371,7 +12413,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", @@ -12785,7 +12828,8 @@ Object { "collaborators": Map {}, "currentItemBackgroundColor": "transparent", "currentItemFillStyle": "hachure", - "currentItemFont": "20px Virgil", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, "currentItemStrokeColor": "#000000", diff --git a/src/types.ts b/src/types.ts index c4316c5d..b25a60de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ import { NonDeleted, TextAlign, ExcalidrawElement, + FontFamily, GroupId, } from "./element/types"; import { SHAPES } from "./shapes"; @@ -35,7 +36,8 @@ export type AppState = { currentItemStrokeStyle: ExcalidrawElement["strokeStyle"]; currentItemRoughness: number; currentItemOpacity: number; - currentItemFont: string; + currentItemFontFamily: FontFamily; + currentItemFontSize: number; currentItemTextAlign: TextAlign; viewBackgroundColor: string; scrollX: FlooredNumber; diff --git a/src/utils.ts b/src/utils.ts index d722e1a1..9fc511a4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import { FlooredNumber } from "./types"; import { getZoomOrigin } from "./scene"; -import { CURSOR_TYPE } from "./constants"; +import { CURSOR_TYPE, FONT_FAMILY } from "./constants"; +import { FontFamily, FontString } from "./element/types"; export const SVG_NS = "http://www.w3.org/2000/svg"; @@ -60,8 +61,27 @@ export const isWritableElement = ( (target instanceof HTMLInputElement && (target.type === "text" || target.type === "number")); +export const getFontFamilyString = ({ + fontFamily, +}: { + fontFamily: FontFamily; +}) => { + return FONT_FAMILY[fontFamily]; +}; + +/** returns fontSize+fontFamily string for assignment to DOM elements */ +export const getFontString = ({ + fontSize, + fontFamily, +}: { + fontSize: number; + fontFamily: FontFamily; +}) => { + return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString; +}; + // https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js -export const measureText = (text: string, font: string) => { +export const measureText = (text: string, font: FontString) => { const line = document.createElement("div"); const body = document.body; line.style.position = "absolute";