Batching read/write baseline DOM measurements to avoid layour thrashing on resize, changed baseline calc. to not rely on absolute positioning, other related perf. improvements
This commit is contained in:
parent
0513b647ec
commit
2939b03e78
@ -207,11 +207,16 @@ const restoreElement = (
|
|||||||
: // no element height likely means programmatic use, so default
|
: // no element height likely means programmatic use, so default
|
||||||
// to a fixed line height
|
// to a fixed line height
|
||||||
getDefaultLineHeight(element.fontFamily));
|
getDefaultLineHeight(element.fontFamily));
|
||||||
|
|
||||||
const baseline = measureBaseline(
|
const baseline = measureBaseline(
|
||||||
element.text,
|
element.text,
|
||||||
getFontString(element),
|
getFontString({
|
||||||
lineHeight,
|
fontSize: element.fontSize,
|
||||||
|
fontFamily: element.fontFamily,
|
||||||
|
}),
|
||||||
|
String(lineHeight),
|
||||||
);
|
);
|
||||||
|
|
||||||
element = restoreElementWithProperties(element, {
|
element = restoreElementWithProperties(element, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
|
@ -54,6 +54,8 @@ import {
|
|||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
measureText,
|
measureText,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
|
measureBaselines,
|
||||||
|
BaselineInput,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
|
||||||
@ -769,6 +771,25 @@ export const resizeMultipleElements = (
|
|||||||
};
|
};
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
|
const precomputedBaselines = measureBaselines(
|
||||||
|
targetElements.reduce((inputs, { latest: element }) => {
|
||||||
|
if (!isTextElement(element)) {
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs.push({
|
||||||
|
id: element.id,
|
||||||
|
text: element.text,
|
||||||
|
font: getFontString({
|
||||||
|
fontSize: element.fontSize,
|
||||||
|
fontFamily: element.fontFamily,
|
||||||
|
}),
|
||||||
|
lineHeight: String(element.lineHeight),
|
||||||
|
});
|
||||||
|
return inputs;
|
||||||
|
}, [] as BaselineInput[]),
|
||||||
|
);
|
||||||
|
|
||||||
for (const { orig, latest } of targetElements) {
|
for (const { orig, latest } of targetElements) {
|
||||||
// bounded text elements are updated along with their container elements
|
// bounded text elements are updated along with their container elements
|
||||||
if (isTextElement(orig) && isBoundToContainer(orig)) {
|
if (isTextElement(orig) && isBoundToContainer(orig)) {
|
||||||
@ -838,17 +859,13 @@ export const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(orig)) {
|
if (isTextElement(orig)) {
|
||||||
const metrics = measureFontSizeFromWidth(
|
const nextFontSize = orig.fontSize * scale;
|
||||||
orig,
|
if (nextFontSize < MIN_FONT_SIZE) {
|
||||||
elementsMap,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
);
|
|
||||||
if (!metrics) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
update.fontSize = metrics.size;
|
|
||||||
update.baseline = metrics.baseline;
|
update.fontSize = nextFontSize;
|
||||||
|
update.baseline = precomputedBaselines.get(orig.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = originalElements.get(
|
const boundTextElement = originalElements.get(
|
||||||
|
@ -294,49 +294,74 @@ export const measureText = (
|
|||||||
const fontSize = parseFloat(font);
|
const fontSize = parseFloat(font);
|
||||||
const height = getTextHeight(text, fontSize, lineHeight);
|
const height = getTextHeight(text, fontSize, lineHeight);
|
||||||
const width = getTextWidth(text, font);
|
const width = getTextWidth(text, font);
|
||||||
const baseline = measureBaseline(text, font, lineHeight);
|
const baseline = measureBaseline(text, font, String(lineHeight));
|
||||||
return { width, height, baseline };
|
return { width, height, baseline };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const measureBaseline = (
|
export type BaselineInput = {
|
||||||
text: string,
|
id: string;
|
||||||
font: FontString,
|
font: FontString;
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: string;
|
||||||
wrapInContainer?: boolean,
|
text: string;
|
||||||
) => {
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baseline calculation is based on expensive DOM operations, resulting in forced reflow.
|
||||||
|
* Therefore whenever we can, we should always batch the calculation of the baselines upfront for all the elements.
|
||||||
|
*/
|
||||||
|
export const measureBaselines = (inputs: BaselineInput[]) => {
|
||||||
|
const baselines = new Map<string, number>();
|
||||||
|
const containers = new Map<string, [HTMLDivElement, BaselineInput]>();
|
||||||
|
|
||||||
|
// Batch DOM writes (and reads below) to avoid layout trashing
|
||||||
|
for (const input of inputs) {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.style.position = "absolute";
|
const span = document.createElement("span");
|
||||||
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);
|
Object.assign(span.style, {
|
||||||
|
display: "inline-block",
|
||||||
|
// overflow: "hidden",
|
||||||
|
});
|
||||||
|
|
||||||
container.innerText = text;
|
Object.assign(container.style, {
|
||||||
|
font: input.font,
|
||||||
|
lineHeight: input.lineHeight,
|
||||||
|
minHeight: "1em",
|
||||||
|
visibility: "hidden",
|
||||||
|
// whitespace: "pre",
|
||||||
|
// overflow: "hidden",
|
||||||
|
// wordBreak: "break-word",
|
||||||
|
// whiteSpace: "pre-wrap",
|
||||||
|
});
|
||||||
|
|
||||||
// Baseline is important for positioning text on canvas
|
container.innerText = input.text;
|
||||||
|
|
||||||
|
container.appendChild(span);
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
const span = document.createElement("span");
|
containers.set(input.id, [container, input]);
|
||||||
span.style.display = "inline-block";
|
}
|
||||||
span.style.overflow = "hidden";
|
|
||||||
span.style.width = "1px";
|
for (const [id, [container, input]] of containers.entries()) {
|
||||||
span.style.height = "1px";
|
const span = container.lastChild as HTMLSpanElement;
|
||||||
container.appendChild(span);
|
let baseline =
|
||||||
let baseline = span.offsetTop + span.offsetHeight;
|
span.getBoundingClientRect().y - container.getBoundingClientRect().y;
|
||||||
const height = container.offsetHeight;
|
|
||||||
|
|
||||||
if (isSafari) {
|
if (isSafari) {
|
||||||
const canvasHeight = getTextHeight(text, parseFloat(font), lineHeight);
|
const height = container.offsetHeight;
|
||||||
const fontSize = parseFloat(font);
|
const fontSize = parseFloat(input.font);
|
||||||
|
const canvasHeight = getTextHeight(
|
||||||
|
input.text,
|
||||||
|
fontSize,
|
||||||
|
Number(input.lineHeight) as any,
|
||||||
|
);
|
||||||
// In Safari the font size gets rounded off when rendering hence calculating the safari height and shifting the baseline if it differs
|
// 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
|
// from the actual canvas height
|
||||||
const domHeight = getTextHeight(text, Math.round(fontSize), lineHeight);
|
const domHeight = getTextHeight(
|
||||||
|
input.text,
|
||||||
|
Math.round(fontSize),
|
||||||
|
Number(input.lineHeight) as any,
|
||||||
|
);
|
||||||
if (canvasHeight > height) {
|
if (canvasHeight > height) {
|
||||||
baseline += canvasHeight - domHeight;
|
baseline += canvasHeight - domHeight;
|
||||||
}
|
}
|
||||||
@ -345,8 +370,26 @@ export const measureBaseline = (
|
|||||||
baseline -= domHeight - canvasHeight;
|
baseline -= domHeight - canvasHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baselines.set(id, baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [container] of containers.values()) {
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
return baseline;
|
}
|
||||||
|
|
||||||
|
return baselines;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const measureBaseline = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: string,
|
||||||
|
) => {
|
||||||
|
// In single measurement the element id is irrelevant
|
||||||
|
const fakeId = "fake-id";
|
||||||
|
const baselines = measureBaselines([{ id: fakeId, text, font, lineHeight }]);
|
||||||
|
return baselines.get(fakeId)!;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user