diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx
index 697d569154..e56ffc10af 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 }) => (
-
- ),
+ 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 (
+
+ );
+ },
});
export const actionChangeTextAlign = register({
diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts
index 9d6cbbe737..17f3758ff4 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 d8a9c26e83..92e211110d 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 7d50315fec..db5cc52cad 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 5e84160fcc..1e1c33b1ff 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 f005c043fe..8ac8608b57 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 afd98906e3..96bf71e8b6 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 1199c5dfe4..a7de6066f0 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 2256506876..68da072da5 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 394faee69f..7a92e7c337 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 3942bf003f..7454c698be 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 08a42418f4..d4567e501d 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 18339fb186..a965900d7c 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 4facda45fa..3caa6bcc5e 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 7a3515fd20..2d4b68c812 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 c4316c5df1..b25a60dedc 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 d722e1a188..9fc511a41f 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";