@ -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 , font Size, 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 ] ;
} ;