|
@@ -4,6 +4,7 @@ import {
|
|
|
ExcalidrawTextContainer,
|
|
|
ExcalidrawTextElement,
|
|
|
ExcalidrawTextElementWithContainer,
|
|
|
+ FontFamilyValues,
|
|
|
FontString,
|
|
|
NonDeletedExcalidrawElement,
|
|
|
} from "./types";
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
BOUND_TEXT_PADDING,
|
|
|
DEFAULT_FONT_FAMILY,
|
|
|
DEFAULT_FONT_SIZE,
|
|
|
+ FONT_FAMILY,
|
|
|
TEXT_ALIGN,
|
|
|
VERTICAL_ALIGN,
|
|
|
} from "../constants";
|
|
@@ -41,12 +43,15 @@ export const normalizeText = (text: string) => {
|
|
|
);
|
|
|
};
|
|
|
|
|
|
+export const splitIntoLines = (text: string) => {
|
|
|
+ return normalizeText(text).split("\n");
|
|
|
+};
|
|
|
+
|
|
|
export const redrawTextBoundingBox = (
|
|
|
textElement: ExcalidrawTextElement,
|
|
|
container: ExcalidrawElement | null,
|
|
|
) => {
|
|
|
let maxWidth = undefined;
|
|
|
-
|
|
|
const boundTextUpdates = {
|
|
|
x: textElement.x,
|
|
|
y: textElement.y,
|
|
@@ -68,6 +73,7 @@ export const redrawTextBoundingBox = (
|
|
|
const metrics = measureText(
|
|
|
boundTextUpdates.text,
|
|
|
getFontString(textElement),
|
|
|
+ textElement.lineHeight,
|
|
|
);
|
|
|
|
|
|
boundTextUpdates.width = metrics.width;
|
|
@@ -185,7 +191,11 @@ export const handleBindTextResize = (
|
|
|
maxWidth,
|
|
|
);
|
|
|
}
|
|
|
- const dimensions = measureText(text, getFontString(textElement));
|
|
|
+ const dimensions = measureText(
|
|
|
+ text,
|
|
|
+ getFontString(textElement),
|
|
|
+ textElement.lineHeight,
|
|
|
+ );
|
|
|
nextHeight = dimensions.height;
|
|
|
nextWidth = dimensions.width;
|
|
|
}
|
|
@@ -261,32 +271,52 @@ const computeBoundTextPosition = (
|
|
|
|
|
|
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
|
|
|
|
|
-export const measureText = (text: string, font: FontString) => {
|
|
|
+export const measureText = (
|
|
|
+ text: string,
|
|
|
+ font: FontString,
|
|
|
+ lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
|
+) => {
|
|
|
text = text
|
|
|
.split("\n")
|
|
|
// replace empty lines with single space because leading/trailing empty
|
|
|
// lines would be stripped from computation
|
|
|
.map((x) => x || " ")
|
|
|
.join("\n");
|
|
|
-
|
|
|
- const height = getTextHeight(text, font);
|
|
|
+ const fontSize = parseFloat(font);
|
|
|
+ const height = getTextHeight(text, fontSize, lineHeight);
|
|
|
const width = getTextWidth(text, font);
|
|
|
|
|
|
return { width, height };
|
|
|
};
|
|
|
|
|
|
-const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
|
|
-const cacheApproxLineHeight: { [key: FontString]: number } = {};
|
|
|
-
|
|
|
-export const getApproxLineHeight = (font: FontString) => {
|
|
|
- if (cacheApproxLineHeight[font]) {
|
|
|
- return cacheApproxLineHeight[font];
|
|
|
- }
|
|
|
- const fontSize = parseInt(font);
|
|
|
+/**
|
|
|
+ * To get unitless line-height (if unknown) we can calculate it by dividing
|
|
|
+ * height-per-line by fontSize.
|
|
|
+ */
|
|
|
+export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
|
|
|
+ const lineCount = splitIntoLines(textElement.text).length;
|
|
|
+ return (textElement.height /
|
|
|
+ lineCount /
|
|
|
+ textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * We calculate the line height from the font size and the unitless line height,
|
|
|
+ * aligning with the W3C spec.
|
|
|
+ */
|
|
|
+export const getLineHeightInPx = (
|
|
|
+ fontSize: ExcalidrawTextElement["fontSize"],
|
|
|
+ lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
|
+) => {
|
|
|
+ return fontSize * lineHeight;
|
|
|
+};
|
|
|
|
|
|
- // Calculate line height relative to font size
|
|
|
- cacheApproxLineHeight[font] = fontSize * 1.2;
|
|
|
- return cacheApproxLineHeight[font];
|
|
|
+// FIXME rename to getApproxMinContainerHeight
|
|
|
+export const getApproxMinLineHeight = (
|
|
|
+ fontSize: ExcalidrawTextElement["fontSize"],
|
|
|
+ lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
|
+) => {
|
|
|
+ return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
|
|
};
|
|
|
|
|
|
let canvas: HTMLCanvasElement | undefined;
|
|
@@ -309,7 +339,7 @@ const getLineWidth = (text: string, font: FontString) => {
|
|
|
};
|
|
|
|
|
|
export const getTextWidth = (text: string, font: FontString) => {
|
|
|
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
|
+ const lines = splitIntoLines(text);
|
|
|
let width = 0;
|
|
|
lines.forEach((line) => {
|
|
|
width = Math.max(width, getLineWidth(line, font));
|
|
@@ -317,10 +347,13 @@ export const getTextWidth = (text: string, font: FontString) => {
|
|
|
return width;
|
|
|
};
|
|
|
|
|
|
-export const getTextHeight = (text: string, font: FontString) => {
|
|
|
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
|
- const lineHeight = getApproxLineHeight(font);
|
|
|
- return lineHeight * lines.length;
|
|
|
+export const getTextHeight = (
|
|
|
+ text: string,
|
|
|
+ fontSize: number,
|
|
|
+ lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
|
+) => {
|
|
|
+ const lineCount = splitIntoLines(text).length;
|
|
|
+ return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
|
|
};
|
|
|
|
|
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|
@@ -468,21 +501,23 @@ export const charWidth = (() => {
|
|
|
};
|
|
|
})();
|
|
|
|
|
|
-export const getApproxMinLineWidth = (font: FontString) => {
|
|
|
+const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
|
|
+
|
|
|
+// FIXME rename to getApproxMinContainerWidth
|
|
|
+export const getApproxMinLineWidth = (
|
|
|
+ font: FontString,
|
|
|
+ lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
|
+) => {
|
|
|
const maxCharWidth = getMaxCharWidth(font);
|
|
|
if (maxCharWidth === 0) {
|
|
|
return (
|
|
|
- measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
|
|
+ measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
|
|
BOUND_TEXT_PADDING * 2
|
|
|
);
|
|
|
}
|
|
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
|
|
};
|
|
|
|
|
|
-export const getApproxMinLineHeight = (font: FontString) => {
|
|
|
- return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
|
|
|
-};
|
|
|
-
|
|
|
export const getMinCharWidth = (font: FontString) => {
|
|
|
const cache = charWidth.getCache(font);
|
|
|
if (!cache) {
|
|
@@ -828,3 +863,32 @@ export const isMeasureTextSupported = () => {
|
|
|
);
|
|
|
return width > 0;
|
|
|
};
|
|
|
+
|
|
|
+/**
|
|
|
+ * Unitless line height
|
|
|
+ *
|
|
|
+ * In previous versions we used `normal` line height, which browsers interpret
|
|
|
+ * differently, and based on font-family and font-size.
|
|
|
+ *
|
|
|
+ * To make line heights consistent across browsers we hardcode the values for
|
|
|
+ * each of our fonts based on most common average line-heights.
|
|
|
+ * See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
|
|
|
+ * where the values come from.
|
|
|
+ */
|
|
|
+const DEFAULT_LINE_HEIGHT = {
|
|
|
+ // ~1.25 is the average for Virgil in WebKit and Blink.
|
|
|
+ // Gecko (FF) uses ~1.28.
|
|
|
+ [FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
|
|
|
+ // ~1.15 is the average for Virgil in WebKit and Blink.
|
|
|
+ // Gecko if all over the place.
|
|
|
+ [FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
|
|
|
+ // ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
|
|
|
+ [FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
|
|
|
+};
|
|
|
+
|
|
|
+export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
|
|
|
+ if (fontFamily) {
|
|
|
+ return DEFAULT_LINE_HEIGHT[fontFamily];
|
|
|
+ }
|
|
|
+ return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
|
|
+};
|