Bladeren bron

split font into fontSize and fontFamily (#1635)

David Luzar 4 jaren geleden
bovenliggende
commit
63c10743fb

+ 32 - 31
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 }) => (
-    <fieldset>
-      <legend>{t("labels.fontFamily")}</legend>
-      <ButtonSelect
-        group="font-family"
-        options={[
-          { value: "Virgil", text: t("labels.handDrawn") },
-          { value: "Helvetica", text: t("labels.normal") },
-          { value: "Cascadia", text: t("labels.code") },
-        ]}
-        value={getFormValue(
-          elements,
-          appState,
-          (element) => isTextElement(element) && element.font.split("px ")[1],
-          (appState.currentItemFont || DEFAULT_FONT).split("px ")[1],
-        )}
-        onChange={(value) => updateData(value)}
-      />
-    </fieldset>
-  ),
+  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 (
+      <fieldset>
+        <legend>{t("labels.fontFamily")}</legend>
+        <ButtonSelect<FontFamily | false>
+          group="font-family"
+          options={options}
+          value={getFormValue(
+            elements,
+            appState,
+            (element) => isTextElement(element) && element.fontFamily,
+            appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
+          )}
+          onChange={(value) => updateData(value)}
+        />
+      </fieldset>
+    );
+  },
 });
 
 export const actionChangeTextAlign = register({

+ 7 - 2
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);

+ 5 - 2
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,

+ 6 - 3
src/components/App.tsx

@@ -751,7 +751,8 @@ class App extends React.Component<any, AppState> {
       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<any, AppState> {
       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<any, AppState> {
             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,
           });
 

+ 7 - 0
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;

+ 1 - 1
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),

+ 30 - 2
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<ExcalidrawTextElement>).fontSize = parseInt(
+              fontPx,
+              10,
+            );
+            (element as Mutable<
+              ExcalidrawTextElement
+            >).fontFamily = getFontFamilyByName(fontFamily);
+            delete (element as any).font;
+          }
           if (!element.textAlign) {
             element.textAlign = DEFAULT_TEXT_ALIGN;
           }

+ 2 - 1
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",
   });
 

+ 7 - 4
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<ExcalidrawTextElement> => {
-  const metrics = measureText(opts.text, opts.font);
+  const metrics = measureText(opts.text, getFontString(opts));
   const textElement = newElementWith(
     {
       ..._newElementBase<ExcalidrawTextElement>("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,

+ 2 - 2
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,

+ 8 - 5
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,

+ 6 - 1
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<ExcalidrawElement>;
 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" };

+ 4 - 11
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;");

+ 10 - 4
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 = `<!-- svg-source:excalidraw -->`;
 
@@ -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,

+ 88 - 44
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",

+ 3 - 1
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;

+ 22 - 2
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";