From 7e471b55ebd45663c7708f3c2e29eebe43fe6d97 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Tue, 5 Mar 2024 19:33:27 +0000 Subject: [PATCH] feat: text measurements based on font metrics (#7693) * Introduced vertical offset based on harcoded font metrics * Unified usage of alphabetic baseline for both canvas & svg export * Removed baseline property * Removed font-size rounding on Safari * Removed artificial width offset --- packages/excalidraw/CHANGELOG.md | 2 + .../excalidraw/actions/actionBoundText.tsx | 3 +- .../data/__snapshots__/transform.test.ts.snap | 19 --- packages/excalidraw/data/restore.ts | 9 +- packages/excalidraw/element/newElement.ts | 13 +- packages/excalidraw/element/resizeElements.ts | 35 +----- packages/excalidraw/element/textElement.ts | 112 ++++++++---------- packages/excalidraw/element/textWysiwyg.tsx | 17 +-- packages/excalidraw/element/types.ts | 1 - packages/excalidraw/renderer/renderElement.ts | 12 +- .../excalidraw/renderer/staticSvgScene.ts | 10 +- .../linearElementEditor.test.tsx.snap | 2 +- .../data/__snapshots__/restore.test.ts.snap | 2 - 13 files changed, 83 insertions(+), 154 deletions(-) diff --git a/packages/excalidraw/CHANGELOG.md b/packages/excalidraw/CHANGELOG.md index 3759b44c4..d88bae8ab 100644 --- a/packages/excalidraw/CHANGELOG.md +++ b/packages/excalidraw/CHANGELOG.md @@ -27,6 +27,8 @@ Please add the latest change on the top under the correct section. - `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539) +- `ExcalidrawTextElement.baseline` was removed and replaced with a vertical offset computation based on font metrics, performed on each text element re-render. + - Create an `ESM` build for `@excalidraw/excalidraw`. The API is in progress and subject to change before stable release. There are some changes on how the package will be consumed #### Bundler diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index e0ea95cd4..daefa5691 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -49,7 +49,7 @@ export const actionUnbindText = register({ selectedElements.forEach((element) => { const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { - const { width, height, baseline } = measureText( + const { width, height } = measureText( boundTextElement.originalText, getFontString(boundTextElement), boundTextElement.lineHeight, @@ -67,7 +67,6 @@ export const actionUnbindText = register({ containerId: null, width, height, - baseline, text: boundTextElement.originalText, x, y, diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap index 450fce7de..2ae7eced8 100644 --- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -224,7 +224,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": [ { "id": "id48", @@ -269,7 +268,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": [ { "id": "id48", @@ -373,7 +371,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id48", "customData": undefined, @@ -472,7 +469,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id37", "customData": undefined, @@ -643,7 +639,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id41", "customData": undefined, @@ -683,7 +678,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": [ { "id": "id41", @@ -728,7 +722,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": [ { "id": "id41", @@ -1174,7 +1167,6 @@ exports[`Test Transform > should transform text element 1`] = ` { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": null, "customData": undefined, @@ -1214,7 +1206,6 @@ exports[`Test Transform > should transform text element 2`] = ` { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": null, "customData": undefined, @@ -1458,7 +1449,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id25", "customData": undefined, @@ -1498,7 +1488,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id26", "customData": undefined, @@ -1538,7 +1527,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id27", "customData": undefined, @@ -1579,7 +1567,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id28", "customData": undefined, @@ -1836,7 +1823,6 @@ exports[`Test Transform > should transform to text containers when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id13", "customData": undefined, @@ -1876,7 +1862,6 @@ exports[`Test Transform > should transform to text containers when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id14", "customData": undefined, @@ -1917,7 +1902,6 @@ exports[`Test Transform > should transform to text containers when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id15", "customData": undefined, @@ -1960,7 +1944,6 @@ exports[`Test Transform > should transform to text containers when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id16", "customData": undefined, @@ -2001,7 +1984,6 @@ exports[`Test Transform > should transform to text containers when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id17", "customData": undefined, @@ -2043,7 +2025,6 @@ exports[`Test Transform > should transform to text containers when label provide { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": null, "containerId": "id18", "customData": undefined, diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 022457f01..0ff0203dc 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -35,14 +35,13 @@ import { import { getDefaultAppState } from "../appState"; import { LinearElementEditor } from "../element/linearElementEditor"; import { bumpVersion } from "../element/mutateElement"; -import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils"; +import { getUpdatedTimestamp, updateActiveTool } from "../utils"; import { arrayToMap } from "../utils"; import { MarkOptional, Mutable } from "../utility-types"; import { detectLineHeight, getContainerElement, getDefaultLineHeight, - measureBaseline, } from "../element/textElement"; import { normalizeLink } from "./url"; @@ -207,11 +206,6 @@ const restoreElement = ( : // no element height likely means programmatic use, so default // to a fixed line height getDefaultLineHeight(element.fontFamily)); - const baseline = measureBaseline( - element.text, - getFontString(element), - lineHeight, - ); element = restoreElementWithProperties(element, { fontSize, fontFamily, @@ -222,7 +216,6 @@ const restoreElement = ( originalText: element.originalText || text, lineHeight, - baseline, }); // if empty text, mark as deleted. We keep in array diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index 076f64722..967359c5a 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -249,7 +249,6 @@ export const newTextElement = ( y: opts.y - offsets.y, width: metrics.width, height: metrics.height, - baseline: metrics.baseline, containerId: opts.containerId || null, originalText: text, lineHeight, @@ -268,13 +267,12 @@ const getAdjustedDimensions = ( y: number; width: number; height: number; - baseline: number; } => { - const { - width: nextWidth, - height: nextHeight, - baseline: nextBaseline, - } = measureText(nextText, getFontString(element), element.lineHeight); + const { width: nextWidth, height: nextHeight } = measureText( + nextText, + getFontString(element), + element.lineHeight, + ); const { textAlign, verticalAlign } = element; let x: number; let y: number; @@ -328,7 +326,6 @@ const getAdjustedDimensions = ( return { width: nextWidth, height: nextHeight, - baseline: nextBaseline, x: Number.isFinite(x) ? x : element.x, y: Number.isFinite(y) ? y : element.y, }; diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 49724d9eb..e18a4ed25 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -52,8 +52,6 @@ import { handleBindTextResize, getBoundTextMaxWidth, getApproxMinLineHeight, - measureText, - getBoundTextMaxHeight, } from "./textElement"; import { LinearElementEditor } from "./linearElementEditor"; @@ -213,8 +211,7 @@ const measureFontSizeFromWidth = ( element: NonDeleted, elementsMap: ElementsMap, nextWidth: number, - nextHeight: number, -): { size: number; baseline: number } | null => { +): { size: number } | null => { // We only use width to scale font on resize let width = element.width; @@ -229,14 +226,9 @@ const measureFontSizeFromWidth = ( if (nextFontSize < MIN_FONT_SIZE) { return null; } - const metrics = measureText( - element.text, - getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }), - element.lineHeight, - ); + return { size: nextFontSize, - baseline: metrics.baseline + (nextHeight - metrics.height), }; }; @@ -309,12 +301,7 @@ const resizeSingleTextElement = ( if (scale > 0) { const nextWidth = element.width * scale; const nextHeight = element.height * scale; - const metrics = measureFontSizeFromWidth( - element, - elementsMap, - nextWidth, - nextHeight, - ); + const metrics = measureFontSizeFromWidth(element, elementsMap, nextWidth); if (metrics === null) { return; } @@ -342,7 +329,6 @@ const resizeSingleTextElement = ( fontSize: metrics.size, width: nextWidth, height: nextHeight, - baseline: metrics.baseline, x: nextElementX, y: nextElementY, }); @@ -396,7 +382,7 @@ export const resizeSingleElement = ( let scaleX = atStartBoundsWidth / boundsCurrentWidth; let scaleY = atStartBoundsHeight / boundsCurrentHeight; - let boundTextFont: { fontSize?: number; baseline?: number } = {}; + let boundTextFont: { fontSize?: number } = {}; const boundTextElement = getBoundTextElement(element, elementsMap); if (transformHandleDirection.includes("e")) { @@ -448,7 +434,6 @@ export const resizeSingleElement = ( if (stateOfBoundTextElementAtResize) { boundTextFont = { fontSize: stateOfBoundTextElementAtResize.fontSize, - baseline: stateOfBoundTextElementAtResize.baseline, }; } if (shouldMaintainAspectRatio) { @@ -462,14 +447,12 @@ export const resizeSingleElement = ( boundTextElement, elementsMap, getBoundTextMaxWidth(updatedElement, boundTextElement), - getBoundTextMaxHeight(updatedElement, boundTextElement), ); if (nextFont === null) { return; } boundTextFont = { fontSize: nextFont.size, - baseline: nextFont.baseline, }; } else { const minWidth = getApproxMinLineWidth( @@ -638,7 +621,6 @@ export const resizeSingleElement = ( if (boundTextElement && boundTextFont != null) { mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize, - baseline: boundTextFont.baseline, }); } handleBindTextResize( @@ -769,7 +751,6 @@ export const resizeMultipleElements = ( > & { points?: ExcalidrawLinearElement["points"]; fontSize?: ExcalidrawTextElement["fontSize"]; - baseline?: ExcalidrawTextElement["baseline"]; scale?: ExcalidrawImageElement["scale"]; boundTextFontSize?: ExcalidrawTextElement["fontSize"]; }; @@ -844,17 +825,11 @@ export const resizeMultipleElements = ( } if (isTextElement(orig)) { - const metrics = measureFontSizeFromWidth( - orig, - elementsMap, - width, - height, - ); + const metrics = measureFontSizeFromWidth(orig, elementsMap, width); if (!metrics) { return; } update.fontSize = metrics.size; - update.baseline = metrics.baseline; } const boundTextElement = originalElements.get( diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index 4aa0868d7..102ed681c 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -18,7 +18,6 @@ import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, FONT_FAMILY, - isSafari, TEXT_ALIGN, VERTICAL_ALIGN, } from "../constants"; @@ -62,7 +61,6 @@ export const redrawTextBoundingBox = ( text: textElement.text, width: textElement.width, height: textElement.height, - baseline: textElement.baseline, }; boundTextUpdates.text = textElement.text; @@ -83,7 +81,6 @@ export const redrawTextBoundingBox = ( boundTextUpdates.width = metrics.width; boundTextUpdates.height = metrics.height; - boundTextUpdates.baseline = metrics.baseline; if (container) { const maxContainerHeight = getBoundTextMaxHeight( @@ -188,7 +185,6 @@ export const handleBindTextResize = ( const maxWidth = getBoundTextMaxWidth(container, textElement); const maxHeight = getBoundTextMaxHeight(container, textElement); let containerHeight = container.height; - let nextBaseLine = textElement.baseline; if ( shouldMaintainAspectRatio || (transformHandleType !== "n" && transformHandleType !== "s") @@ -207,7 +203,6 @@ export const handleBindTextResize = ( ); nextHeight = metrics.height; nextWidth = metrics.width; - nextBaseLine = metrics.baseline; } // increase height in case text element height exceeds if (nextHeight > maxHeight) { @@ -235,7 +230,6 @@ export const handleBindTextResize = ( text, width: nextWidth, height: nextHeight, - baseline: nextBaseLine, }); if (!isArrowElement(container)) { @@ -285,8 +279,6 @@ export const computeBoundTextPosition = ( return { x, y }; }; -// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js - export const measureText = ( text: string, font: FontString, @@ -301,59 +293,7 @@ export const measureText = ( const fontSize = parseFloat(font); const height = getTextHeight(text, fontSize, lineHeight); const width = getTextWidth(text, font); - const baseline = measureBaseline(text, font, lineHeight); - return { width, height, baseline }; -}; - -export const measureBaseline = ( - text: string, - font: FontString, - lineHeight: ExcalidrawTextElement["lineHeight"], - wrapInContainer?: boolean, -) => { - const container = document.createElement("div"); - container.style.position = "absolute"; - container.style.whiteSpace = "pre"; - container.style.font = font; - container.style.minHeight = "1em"; - if (wrapInContainer) { - container.style.overflow = "hidden"; - container.style.wordBreak = "break-word"; - container.style.whiteSpace = "pre-wrap"; - } - - container.style.lineHeight = String(lineHeight); - - container.innerText = text; - - // Baseline is important for positioning text on canvas - document.body.appendChild(container); - - const span = document.createElement("span"); - span.style.display = "inline-block"; - span.style.overflow = "hidden"; - span.style.width = "1px"; - span.style.height = "1px"; - container.appendChild(span); - let baseline = span.offsetTop + span.offsetHeight; - const height = container.offsetHeight; - - if (isSafari) { - const canvasHeight = getTextHeight(text, parseFloat(font), lineHeight); - const fontSize = parseFloat(font); - // In Safari the font size gets rounded off when rendering hence calculating the safari height and shifting the baseline if it differs - // from the actual canvas height - const domHeight = getTextHeight(text, Math.round(fontSize), lineHeight); - if (canvasHeight > height) { - baseline += canvasHeight - domHeight; - } - - if (height > canvasHeight) { - baseline -= domHeight - canvasHeight; - } - } - document.body.removeChild(container); - return baseline; + return { width, height }; }; /** @@ -378,6 +318,23 @@ export const getLineHeightInPx = ( return fontSize * lineHeight; }; +/** + * Calculates vertical offset for a text with alphabetic baseline. + */ +export const getVerticalOffset = ( + fontFamily: ExcalidrawTextElement["fontFamily"], + fontSize: ExcalidrawTextElement["fontSize"], + lineHeightPx: number, +) => { + const { unitsPerEm, ascender, descender } = FONT_METRICS[fontFamily]; + + const fontSizeEm = fontSize / unitsPerEm; + const lineGap = lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender; + + const verticalOffset = fontSizeEm * ascender + lineGap; + return verticalOffset; +}; + // FIXME rename to getApproxMinContainerHeight export const getApproxMinLineHeight = ( fontSize: ExcalidrawTextElement["fontSize"], @@ -964,13 +921,40 @@ 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. + // ~1.15 is the average for Helvetica in WebKit and Blink. [FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"], - // ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too + // ~1.2 is the average for Cascadia in WebKit and Blink, and kinda Gecko too [FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"], }; +type FontMetrics = { + unitsPerEm: number; // head.unitsPerEm + ascender: number; // sTypoAscender + descender: number; // sTypoDescender +}; + +/** + * Hardcoded metrics for default fonts, read by https://opentype.js.org/font-inspector.html. + * For custom fonts, read these metrics on load and extend this object. + */ +const FONT_METRICS = { + [FONT_FAMILY.Virgil]: { + unitsPerEm: 1000, + ascender: 886, + descender: -374, + }, + [FONT_FAMILY.Helvetica]: { + unitsPerEm: 2048, + ascender: 1577, + descender: -471, + }, + [FONT_FAMILY.Cascadia]: { + unitsPerEm: 2048, + ascender: 1977, + descender: -480, + }, +} as Record; + export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => { if (fontFamily in DEFAULT_LINE_HEIGHT) { return DEFAULT_LINE_HEIGHT[fontFamily]; diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index ae30be4e9..7dfdbc615 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -11,7 +11,7 @@ import { isBoundToContainer, isTextElement, } from "./typeChecks"; -import { CLASSES, isSafari } from "../constants"; +import { CLASSES } from "../constants"; import { ExcalidrawElement, ExcalidrawLinearElement, @@ -31,7 +31,6 @@ import { getBoundTextMaxHeight, getBoundTextMaxWidth, computeContainerDimensionForBoundText, - detectLineHeight, computeBoundTextPosition, getBoundTextElement, } from "./textElement"; @@ -227,18 +226,6 @@ export const textWysiwyg = ({ if (!container) { maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value; textElementWidth = Math.min(textElementWidth, maxWidth); - } else { - textElementWidth += 0.5; - } - - let lineHeight = updatedTextElement.lineHeight; - - // In Safari the font size gets rounded off when rendering hence calculating the line height by rounding off font size - if (isSafari) { - lineHeight = detectLineHeight({ - ...updatedTextElement, - fontSize: Math.round(updatedTextElement.fontSize), - }); } // Make sure text editor height doesn't go beyond viewport @@ -247,7 +234,7 @@ export const textWysiwyg = ({ Object.assign(editable.style, { font: getFontString(updatedTextElement), // must be defined *after* font ¯\_(ツ)_/¯ - lineHeight, + lineHeight: updatedTextElement.lineHeight, width: `${textElementWidth}px`, height: `${textElementHeight}px`, left: `${viewportX}px`, diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index aae0a8a30..85f42adcf 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -176,7 +176,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase & fontSize: number; fontFamily: FontFamilyValues; text: string; - baseline: number; textAlign: TextAlign; verticalAlign: VerticalAlign; containerId: ExcalidrawGenericElement["id"] | null; diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index a40e3d398..df3e20efe 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -50,6 +50,7 @@ import { getLineHeightInPx, getBoundTextMaxHeight, getBoundTextMaxWidth, + getVerticalOffset, } from "../element/textElement"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -383,16 +384,23 @@ const drawElementOnCanvas = ( : element.textAlign === "right" ? element.width : 0; + const lineHeightPx = getLineHeightInPx( element.fontSize, element.lineHeight, ); - const verticalOffset = element.height - element.baseline; + + const verticalOffset = getVerticalOffset( + element.fontFamily, + element.fontSize, + lineHeightPx, + ); + for (let index = 0; index < lines.length; index++) { context.fillText( lines[index], horizontalOffset, - (index + 1) * lineHeightPx - verticalOffset, + index * lineHeightPx + verticalOffset, ); } context.restore(); diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index de026300e..7b3917d4e 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -17,6 +17,7 @@ import { getBoundTextElement, getContainerElement, getLineHeightInPx, + getVerticalOffset, } from "../element/textElement"; import { isArrowElement, @@ -556,6 +557,11 @@ const renderElementToSvg = ( : element.textAlign === "right" ? element.width : 0; + const verticalOffset = getVerticalOffset( + element.fontFamily, + element.fontSize, + lineHeightPx, + ); const direction = isRTL(element.text) ? "rtl" : "ltr"; const textAnchor = element.textAlign === "center" @@ -567,14 +573,14 @@ const renderElementToSvg = ( const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text"); text.textContent = lines[i]; text.setAttribute("x", `${horizontalOffset}`); - text.setAttribute("y", `${i * lineHeightPx}`); + text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`); 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;"); text.setAttribute("direction", direction); - text.setAttribute("dominant-baseline", "text-before-edge"); + text.setAttribute("dominant-baseline", "alphabetic"); node.appendChild(text); } diff --git a/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap index 41e9f2b12..1fd7106bd 100644 --- a/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo class="excalidraw-wysiwyg" data-type="wysiwyg" dir="auto" - style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;" + style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;" tabindex="0" wrap="off" /> diff --git a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap index 457ed4f14..156e839a3 100644 --- a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap +++ b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap @@ -296,7 +296,6 @@ exports[`restoreElements > should restore text element correctly passing value f { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": [], "containerId": null, "customData": undefined, @@ -338,7 +337,6 @@ exports[`restoreElements > should restore text element correctly with unknown fo { "angle": 0, "backgroundColor": "transparent", - "baseline": 0, "boundElements": [], "containerId": null, "customData": undefined,