mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-03 12:25:51 +01:00
Compare commits
32 Commits
aakansha-f
...
zsviczian-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b82a0749b1 | ||
|
|
a772362599 | ||
|
|
3f11ca0a44 | ||
|
|
808e4711f9 | ||
|
|
a26e8bade8 | ||
|
|
fc7135c5d1 | ||
|
|
f017a60101 | ||
|
|
17f9f64eda | ||
|
|
619e4061f5 | ||
|
|
028ad1ee81 | ||
|
|
09a05a4a1c | ||
|
|
c4738b31fb | ||
|
|
87e6638e9e | ||
|
|
0a6d41ecf9 | ||
|
|
11109fcc62 | ||
|
|
c7346e3a77 | ||
|
|
fd030de669 | ||
|
|
f77975cee5 | ||
|
|
f994e5d71d | ||
|
|
77028f4d08 | ||
|
|
fb29bb4816 | ||
|
|
23fcddb2a3 | ||
|
|
b314b939b2 | ||
|
|
bc687fea1b | ||
|
|
e15f313fe7 | ||
|
|
9f02922c91 | ||
|
|
2b6819eb2d | ||
|
|
c0e9b8d7bc | ||
|
|
c8c683c025 | ||
|
|
2117fbbc57 | ||
|
|
3b9953f57f | ||
|
|
7d1efb7f8b |
@@ -42,7 +42,7 @@ export const actionUnbindText = register({
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const { width, height, baseline } = measureText(
|
||||
const { width, height } = measureText(
|
||||
boundTextElement.originalText,
|
||||
getFontString(boundTextElement),
|
||||
boundTextElement.lineHeight,
|
||||
@@ -56,7 +56,6 @@ export const actionUnbindText = register({
|
||||
containerId: null,
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
text: boundTextElement.originalText,
|
||||
});
|
||||
mutateElement(element, {
|
||||
|
||||
@@ -1361,6 +1361,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
document.querySelector(".excalidraw")!,
|
||||
).getPropertyValue("--color-selection");
|
||||
|
||||
//const now = Date.now();
|
||||
//if (!this.state.shouldCacheIgnoreZoom) {
|
||||
// console.log(`renderScene`, now);
|
||||
//}
|
||||
renderScene(
|
||||
{
|
||||
elements: renderingElements,
|
||||
@@ -1397,11 +1401,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside });
|
||||
}
|
||||
|
||||
this.scheduleImageRefresh();
|
||||
//if (!this.state.shouldCacheIgnoreZoom) {
|
||||
// setTimeout(() => console.log(`after renderScene`, now));
|
||||
//}
|
||||
},
|
||||
},
|
||||
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
|
||||
true ||
|
||||
(THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true),
|
||||
);
|
||||
|
||||
if (!THROTTLE_NEXT_RENDER) {
|
||||
|
||||
@@ -31,15 +31,11 @@ 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 oc from "open-color";
|
||||
import { MarkOptional, Mutable } from "../utility-types";
|
||||
import {
|
||||
detectLineHeight,
|
||||
getDefaultLineHeight,
|
||||
getDOMMetrics,
|
||||
} from "../element/textElement";
|
||||
import { detectLineHeight, getDefaultLineHeight } from "../element/textElement";
|
||||
|
||||
type RestoredAppState = Omit<
|
||||
AppState,
|
||||
@@ -175,24 +171,6 @@ const restoreElement = (
|
||||
}
|
||||
const text = element.text ?? "";
|
||||
|
||||
// line-height might not be specified either when creating elements
|
||||
// programmatically, or when importing old diagrams.
|
||||
// For the latter we want to detect the original line height which
|
||||
// will likely differ from our per-font fixed line height we now use,
|
||||
// to maintain backward compatibility.
|
||||
const lineHeight =
|
||||
element.lineHeight ||
|
||||
(element.height
|
||||
? // detect line-height from current element height and font-size
|
||||
detectLineHeight(element)
|
||||
: // no element height likely means programmatic use, so default
|
||||
// to a fixed line height
|
||||
getDefaultLineHeight(element.fontFamily));
|
||||
const { baseline } = getDOMMetrics(
|
||||
element.text,
|
||||
getFontString(element),
|
||||
lineHeight,
|
||||
);
|
||||
element = restoreElementWithProperties(element, {
|
||||
fontSize,
|
||||
fontFamily,
|
||||
@@ -201,9 +179,19 @@ const restoreElement = (
|
||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: element.containerId ?? null,
|
||||
originalText: element.originalText || text,
|
||||
|
||||
lineHeight,
|
||||
baseline,
|
||||
// line-height might not be specified either when creating elements
|
||||
// programmatically, or when importing old diagrams.
|
||||
// For the latter we want to detect the original line height which
|
||||
// will likely differ from our per-font fixed line height we now use,
|
||||
// to maintain backward compatibility.
|
||||
lineHeight:
|
||||
element.lineHeight ||
|
||||
(element.height
|
||||
? // detect line-height from current element height and font-size
|
||||
detectLineHeight(element)
|
||||
: // no element height likely means programmatic use, so default
|
||||
// to a fixed line height
|
||||
getDefaultLineHeight(element.fontFamily)),
|
||||
});
|
||||
|
||||
if (refreshDimensions) {
|
||||
|
||||
@@ -145,7 +145,6 @@ export const newTextElement = (
|
||||
const text = normalizeText(opts.text);
|
||||
const metrics = measureText(text, getFontString(opts), lineHeight);
|
||||
const offsets = getTextElementPositionOffsets(opts, metrics);
|
||||
|
||||
const textElement = newElementWith(
|
||||
{
|
||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||
@@ -158,7 +157,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,
|
||||
@@ -176,15 +174,14 @@ const getAdjustedDimensions = (
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
baseline: number;
|
||||
} => {
|
||||
const container = getContainerElement(element);
|
||||
|
||||
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;
|
||||
@@ -259,7 +256,6 @@ const getAdjustedDimensions = (
|
||||
return {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseline,
|
||||
x: Number.isFinite(x) ? x : element.x,
|
||||
y: Number.isFinite(y) ? y : element.y,
|
||||
};
|
||||
|
||||
@@ -46,8 +46,6 @@ import {
|
||||
handleBindTextResize,
|
||||
getMaxContainerWidth,
|
||||
getApproxMinLineHeight,
|
||||
measureText,
|
||||
getMaxContainerHeight,
|
||||
} from "./textElement";
|
||||
|
||||
export const normalizeAngle = (angle: number): number => {
|
||||
@@ -195,8 +193,7 @@ const MIN_FONT_SIZE = 1;
|
||||
const measureFontSizeFromWidth = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
nextWidth: number,
|
||||
nextHeight: number,
|
||||
): { size: number; baseline: number } | null => {
|
||||
): number | null => {
|
||||
// We only use width to scale font on resize
|
||||
let width = element.width;
|
||||
|
||||
@@ -211,15 +208,8 @@ 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),
|
||||
};
|
||||
|
||||
return nextFontSize;
|
||||
};
|
||||
|
||||
const getSidesForTransformHandle = (
|
||||
@@ -290,8 +280,8 @@ const resizeSingleTextElement = (
|
||||
if (scale > 0) {
|
||||
const nextWidth = element.width * scale;
|
||||
const nextHeight = element.height * scale;
|
||||
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
|
||||
if (metrics === null) {
|
||||
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
|
||||
if (nextFontSize === null) {
|
||||
return;
|
||||
}
|
||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||
@@ -315,10 +305,9 @@ const resizeSingleTextElement = (
|
||||
deltaY2,
|
||||
);
|
||||
mutateElement(element, {
|
||||
fontSize: metrics.size,
|
||||
fontSize: nextFontSize,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: metrics.baseline,
|
||||
x: nextElementX,
|
||||
y: nextElementY,
|
||||
});
|
||||
@@ -371,7 +360,7 @@ export const resizeSingleElement = (
|
||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||
|
||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
||||
let boundTextFontSize: number | null = null;
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
@@ -421,10 +410,7 @@ export const resizeSingleElement = (
|
||||
boundTextElement.id,
|
||||
) as typeof boundTextElement | undefined;
|
||||
if (stateOfBoundTextElementAtResize) {
|
||||
boundTextFont = {
|
||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
||||
baseline: stateOfBoundTextElementAtResize.baseline,
|
||||
};
|
||||
boundTextFontSize = stateOfBoundTextElementAtResize.fontSize;
|
||||
}
|
||||
if (shouldMaintainAspectRatio) {
|
||||
const updatedElement = {
|
||||
@@ -433,18 +419,14 @@ export const resizeSingleElement = (
|
||||
height: eleNewHeight,
|
||||
};
|
||||
|
||||
const nextFont = measureFontSizeFromWidth(
|
||||
const nextFontSize = measureFontSizeFromWidth(
|
||||
boundTextElement,
|
||||
getMaxContainerWidth(updatedElement),
|
||||
getMaxContainerHeight(updatedElement),
|
||||
);
|
||||
if (nextFont === null) {
|
||||
if (nextFontSize === null) {
|
||||
return;
|
||||
}
|
||||
boundTextFont = {
|
||||
fontSize: nextFont.size,
|
||||
baseline: nextFont.baseline,
|
||||
};
|
||||
boundTextFontSize = nextFontSize;
|
||||
} else {
|
||||
const minWidth = getApproxMinLineWidth(
|
||||
getFontString(boundTextElement),
|
||||
@@ -586,10 +568,9 @@ export const resizeSingleElement = (
|
||||
});
|
||||
|
||||
mutateElement(element, resizedElement);
|
||||
if (boundTextElement && boundTextFont != null) {
|
||||
if (boundTextElement && boundTextFontSize != null) {
|
||||
mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFont.fontSize,
|
||||
baseline: boundTextFont.baseline,
|
||||
fontSize: boundTextFontSize,
|
||||
});
|
||||
}
|
||||
handleBindTextResize(element, transformHandleDirection);
|
||||
@@ -696,7 +677,6 @@ const resizeMultipleElements = (
|
||||
y: number;
|
||||
points?: Point[];
|
||||
fontSize?: number;
|
||||
baseline?: number;
|
||||
} = {
|
||||
width,
|
||||
height,
|
||||
@@ -705,7 +685,7 @@ const resizeMultipleElements = (
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
|
||||
let boundTextUpdates: { fontSize: number } | null = null;
|
||||
|
||||
const boundTextElement = getBoundTextElement(element.latest);
|
||||
|
||||
@@ -715,29 +695,24 @@ const resizeMultipleElements = (
|
||||
width,
|
||||
height,
|
||||
};
|
||||
const metrics = measureFontSizeFromWidth(
|
||||
const fontSize = measureFontSizeFromWidth(
|
||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||
boundTextElement
|
||||
? getMaxContainerWidth(updatedElement)
|
||||
: updatedElement.width,
|
||||
boundTextElement
|
||||
? getMaxContainerHeight(updatedElement)
|
||||
: updatedElement.height,
|
||||
);
|
||||
|
||||
if (!metrics) {
|
||||
if (!fontSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTextElement(element.orig)) {
|
||||
update.fontSize = metrics.size;
|
||||
update.baseline = metrics.baseline;
|
||||
update.fontSize = fontSize;
|
||||
}
|
||||
|
||||
if (boundTextElement) {
|
||||
boundTextUpdates = {
|
||||
fontSize: metrics.size,
|
||||
baseline: metrics.baseline,
|
||||
fontSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
FONT_FAMILY,
|
||||
isSafari,
|
||||
TEXT_ALIGN,
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
@@ -59,7 +58,6 @@ export const redrawTextBoundingBox = (
|
||||
text: textElement.text,
|
||||
width: textElement.width,
|
||||
height: textElement.height,
|
||||
baseline: textElement.baseline,
|
||||
};
|
||||
|
||||
boundTextUpdates.text = textElement.text;
|
||||
@@ -80,7 +78,6 @@ export const redrawTextBoundingBox = (
|
||||
|
||||
boundTextUpdates.width = metrics.width;
|
||||
boundTextUpdates.height = metrics.height;
|
||||
boundTextUpdates.baseline = metrics.baseline;
|
||||
|
||||
if (container) {
|
||||
if (isArrowElement(container)) {
|
||||
@@ -186,7 +183,6 @@ export const handleBindTextResize = (
|
||||
const maxWidth = getMaxContainerWidth(container);
|
||||
const maxHeight = getMaxContainerHeight(container);
|
||||
let containerHeight = containerDims.height;
|
||||
let nextBaseLine = textElement.baseline;
|
||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||
if (text) {
|
||||
text = wrapText(
|
||||
@@ -195,14 +191,13 @@ export const handleBindTextResize = (
|
||||
maxWidth,
|
||||
);
|
||||
}
|
||||
const metrics = measureText(
|
||||
const dimensions = measureText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
textElement.lineHeight,
|
||||
);
|
||||
nextHeight = metrics.height;
|
||||
nextWidth = metrics.width;
|
||||
nextBaseLine = metrics.baseline;
|
||||
nextHeight = dimensions.height;
|
||||
nextWidth = dimensions.width;
|
||||
}
|
||||
// increase height in case text element height exceeds
|
||||
if (nextHeight > maxHeight) {
|
||||
@@ -230,7 +225,6 @@ export const handleBindTextResize = (
|
||||
text,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseLine,
|
||||
});
|
||||
|
||||
if (!isArrowElement(container)) {
|
||||
@@ -291,60 +285,8 @@ export const measureText = (
|
||||
const fontSize = parseFloat(font);
|
||||
const height = getTextHeight(text, fontSize, lineHeight);
|
||||
const width = getTextWidth(text, font);
|
||||
const { baseline } = getDOMMetrics(text, font, lineHeight);
|
||||
return { width, height, baseline };
|
||||
};
|
||||
|
||||
export const getDOMMetrics = (
|
||||
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);
|
||||
const canvasHeight = getTextHeight(text, parseFloat(font), 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) {
|
||||
// In Safari sometimes DOM height could be less than canvas height due to
|
||||
// which text could go out of the bounding box hence shifting the baseline
|
||||
// to make sure text is rendered correctly
|
||||
if (canvasHeight > height) {
|
||||
baseline += canvasHeight - height;
|
||||
}
|
||||
// In Safari sometimes DOM height could be more than canvas height due to
|
||||
// which text could go out of the bounding box hence shifting the baseline
|
||||
// to make sure text is rendered correctly
|
||||
if (height > canvasHeight) {
|
||||
baseline -= height - canvasHeight;
|
||||
}
|
||||
}
|
||||
document.body.removeChild(container);
|
||||
return { baseline, height };
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -366,9 +308,7 @@ export const getLineHeightInPx = (
|
||||
fontSize: ExcalidrawTextElement["fontSize"],
|
||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||
) => {
|
||||
const res = fontSize * lineHeight;
|
||||
|
||||
return res;
|
||||
return fontSize * lineHeight;
|
||||
};
|
||||
|
||||
// FIXME rename to getApproxMinContainerHeight
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
isBoundToContainer,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants";
|
||||
import { CLASSES, VERTICAL_ALIGN } from "../constants";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
@@ -35,9 +35,6 @@ import {
|
||||
getMaxContainerHeight,
|
||||
getMaxContainerWidth,
|
||||
computeContainerDimensionForBoundText,
|
||||
getDOMMetrics,
|
||||
splitIntoLines,
|
||||
detectLineHeight,
|
||||
} from "./textElement";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
@@ -274,38 +271,25 @@ export const textWysiwyg = ({
|
||||
} else {
|
||||
textElementWidth += 0.5;
|
||||
}
|
||||
const { height: domHeight } = getDOMMetrics(
|
||||
updatedTextElement.text,
|
||||
getFontString(updatedTextElement),
|
||||
updatedTextElement.lineHeight,
|
||||
);
|
||||
|
||||
let lineHeight = element.lineHeight;
|
||||
const fontSize = Math.floor(updatedTextElement.fontSize);
|
||||
|
||||
if (isSafari) {
|
||||
//@ts-ignore
|
||||
lineHeight =
|
||||
updatedTextElement.height /
|
||||
splitIntoLines(updatedTextElement.text).length /
|
||||
fontSize;
|
||||
}
|
||||
|
||||
// Make sure text editor height doesn't go beyond viewport
|
||||
const editorMaxHeight =
|
||||
(appState.height - viewportY) / appState.zoom.value;
|
||||
Object.assign(editable.style, {
|
||||
font: getFontString({
|
||||
fontSize,
|
||||
fontFamily: updatedTextElement.fontFamily,
|
||||
}),
|
||||
font: getFontString(updatedTextElement),
|
||||
// must be defined *after* font ¯\_(ツ)_/¯
|
||||
lineHeight,
|
||||
lineHeight: element.lineHeight,
|
||||
width: `${textElementWidth}px`,
|
||||
height: `${textElementHeight}px`,
|
||||
left: `${viewportX}px`,
|
||||
top: `${viewportY}px`,
|
||||
transform: `scale(${updatedTextElement.fontSize / fontSize})`,
|
||||
transform: getTransform(
|
||||
textElementWidth,
|
||||
textElementHeight,
|
||||
getTextElementAngle(updatedTextElement),
|
||||
appState,
|
||||
maxWidth,
|
||||
editorMaxHeight,
|
||||
),
|
||||
textAlign,
|
||||
verticalAlign,
|
||||
color: updatedTextElement.strokeColor,
|
||||
@@ -313,7 +297,6 @@ export const textWysiwyg = ({
|
||||
filter: "var(--theme-filter)",
|
||||
maxHeight: `${editorMaxHeight}px`,
|
||||
});
|
||||
editable.scrollTop = 0;
|
||||
// For some reason updating font attribute doesn't set font family
|
||||
// hence updating font family explicitly for test environment
|
||||
if (isTestEnv()) {
|
||||
|
||||
@@ -131,7 +131,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||
fontSize: number;
|
||||
fontFamily: FontFamilyValues;
|
||||
text: string;
|
||||
baseline: number;
|
||||
textAlign: TextAlign;
|
||||
verticalAlign: VerticalAlign;
|
||||
containerId: ExcalidrawGenericElement["id"] | null;
|
||||
|
||||
@@ -30,7 +30,7 @@ import { RenderConfig } from "../scene/types";
|
||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
||||
import { AppState, BinaryFiles, NormalizedZoomValue, Zoom } from "../types";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import {
|
||||
BOUND_TEXT_PADDING,
|
||||
@@ -93,6 +93,50 @@ export interface ExcalidrawElementWithCanvas {
|
||||
boundTextElementVersion: number | null;
|
||||
}
|
||||
|
||||
export const cappedElementCanvasSize = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
zoom: Zoom,
|
||||
): {
|
||||
width: number;
|
||||
height: number;
|
||||
zoomValue: NormalizedZoomValue;
|
||||
} => {
|
||||
const sizelimit = 16777216; // 2^24
|
||||
const padding = getCanvasPadding(element);
|
||||
let zoomValue = zoom.value;
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
|
||||
let width = distance(x1, x2) * window.devicePixelRatio + padding * 2;
|
||||
let height = distance(y1, y2) * window.devicePixelRatio + padding * 2;
|
||||
|
||||
const size = width * height * zoomValue * zoomValue;
|
||||
if (size > sizelimit) {
|
||||
zoomValue = Math.sqrt(
|
||||
sizelimit / (width * height),
|
||||
) as NormalizedZoomValue;
|
||||
width = distance(x1, x2) * window.devicePixelRatio + padding * 2;
|
||||
height = distance(y1, y2) * window.devicePixelRatio + padding * 2;
|
||||
}
|
||||
width *= zoomValue;
|
||||
height *= zoomValue;
|
||||
return { width, height, zoomValue };
|
||||
}
|
||||
let width = element.width * window.devicePixelRatio + padding * 2;
|
||||
let height = element.height * window.devicePixelRatio + padding * 2;
|
||||
|
||||
const size = width * height * zoomValue * zoomValue;
|
||||
if (size > sizelimit) {
|
||||
zoomValue = Math.sqrt(sizelimit / (width * height)) as NormalizedZoomValue;
|
||||
width = element.width * window.devicePixelRatio + padding * 2;
|
||||
height = element.height * window.devicePixelRatio + padding * 2;
|
||||
}
|
||||
width *= zoomValue;
|
||||
height *= zoomValue;
|
||||
return { width, height, zoomValue };
|
||||
};
|
||||
|
||||
const generateElementCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
zoom: Zoom,
|
||||
@@ -102,44 +146,35 @@ const generateElementCanvas = (
|
||||
const context = canvas.getContext("2d")!;
|
||||
const padding = getCanvasPadding(element);
|
||||
|
||||
const { width, height, zoomValue } = cappedElementCanvasSize(element, zoom);
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
let canvasOffsetX = 0;
|
||||
let canvasOffsetY = 0;
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
|
||||
canvas.width =
|
||||
distance(x1, x2) * window.devicePixelRatio * zoom.value +
|
||||
padding * zoom.value * 2;
|
||||
canvas.height =
|
||||
distance(y1, y2) * window.devicePixelRatio * zoom.value +
|
||||
padding * zoom.value * 2;
|
||||
const [x1, y1] = getElementAbsoluteCoords(element);
|
||||
|
||||
canvasOffsetX =
|
||||
element.x > x1
|
||||
? distance(element.x, x1) * window.devicePixelRatio * zoom.value
|
||||
? distance(element.x, x1) * window.devicePixelRatio * zoomValue
|
||||
: 0;
|
||||
|
||||
canvasOffsetY =
|
||||
element.y > y1
|
||||
? distance(element.y, y1) * window.devicePixelRatio * zoom.value
|
||||
? distance(element.y, y1) * window.devicePixelRatio * zoomValue
|
||||
: 0;
|
||||
|
||||
context.translate(canvasOffsetX, canvasOffsetY);
|
||||
} else {
|
||||
canvas.width =
|
||||
element.width * window.devicePixelRatio * zoom.value +
|
||||
padding * zoom.value * 2;
|
||||
canvas.height =
|
||||
element.height * window.devicePixelRatio * zoom.value +
|
||||
padding * zoom.value * 2;
|
||||
}
|
||||
|
||||
context.save();
|
||||
context.translate(padding * zoom.value, padding * zoom.value);
|
||||
context.translate(padding * zoomValue, padding * zoomValue);
|
||||
context.scale(
|
||||
window.devicePixelRatio * zoom.value,
|
||||
window.devicePixelRatio * zoom.value,
|
||||
window.devicePixelRatio * zoomValue,
|
||||
window.devicePixelRatio * zoomValue,
|
||||
);
|
||||
|
||||
const rc = rough.canvas(canvas);
|
||||
@@ -156,7 +191,7 @@ const generateElementCanvas = (
|
||||
element,
|
||||
canvas,
|
||||
theme: renderConfig.theme,
|
||||
canvasZoom: zoom.value,
|
||||
canvasZoom: zoomValue,
|
||||
canvasOffsetX,
|
||||
canvasOffsetY,
|
||||
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
||||
@@ -200,6 +235,7 @@ const drawImagePlaceholder = (
|
||||
size,
|
||||
);
|
||||
};
|
||||
|
||||
const drawElementOnCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
@@ -272,10 +308,7 @@ const drawElementOnCanvas = (
|
||||
}
|
||||
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
||||
context.save();
|
||||
context.font = getFontString({
|
||||
...element,
|
||||
fontSize: Math.floor(element.fontSize),
|
||||
});
|
||||
context.font = getFontString(element);
|
||||
context.fillStyle = element.strokeColor;
|
||||
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||
|
||||
@@ -288,16 +321,18 @@ const drawElementOnCanvas = (
|
||||
: element.textAlign === "right"
|
||||
? element.width
|
||||
: 0;
|
||||
context.textBaseline = "bottom";
|
||||
|
||||
const lineHeightPx = getLineHeightInPx(
|
||||
element.fontSize,
|
||||
element.lineHeight,
|
||||
);
|
||||
const verticalOffset = element.height - element.baseline;
|
||||
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
context.fillText(
|
||||
lines[index],
|
||||
horizontalOffset,
|
||||
(index + 1) * lineHeightPx - verticalOffset,
|
||||
(index + 1) * lineHeightPx,
|
||||
);
|
||||
}
|
||||
context.restore();
|
||||
@@ -422,6 +457,11 @@ const generateElementShape = (
|
||||
// `null` indicates no rc shape applicable for this element type
|
||||
// (= do not generate anything)
|
||||
if (shape === undefined) {
|
||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||
if (prevElementWithCanvas?.canvas) {
|
||||
prevElementWithCanvas.canvas.width = 0;
|
||||
prevElementWithCanvas.canvas.height = 0;
|
||||
}
|
||||
elementWithCanvasCache.delete(element);
|
||||
|
||||
switch (element.type) {
|
||||
@@ -685,7 +725,10 @@ const generateElementWithCanvas = (
|
||||
zoom,
|
||||
renderConfig,
|
||||
);
|
||||
|
||||
if (prevElementWithCanvas?.canvas) {
|
||||
prevElementWithCanvas.canvas.width = 0;
|
||||
prevElementWithCanvas.canvas.height = 0;
|
||||
}
|
||||
elementWithCanvasCache.set(element, elementWithCanvas);
|
||||
|
||||
return elementWithCanvas;
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
} from "../scene/scrollbars";
|
||||
import { getSelectedElements } from "../scene/selection";
|
||||
|
||||
import { renderElement, renderElementToSvg } from "./renderElement";
|
||||
import {
|
||||
cappedElementCanvasSize,
|
||||
renderElement,
|
||||
renderElementToSvg,
|
||||
} from "./renderElement";
|
||||
import { getClientColors } from "../clients";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import {
|
||||
@@ -407,6 +411,21 @@ export const _renderScene = ({
|
||||
|
||||
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
|
||||
undefined;
|
||||
const start = Date.now();
|
||||
const showDebug = false; //!appState.shouldCacheIgnoreZoom && (appState.zoom.value < 0.5);
|
||||
if (showDebug) {
|
||||
console.log("start: renderElements");
|
||||
}
|
||||
console.log(
|
||||
visibleElements.length,
|
||||
appState.zoom.value,
|
||||
Math.round(
|
||||
visibleElements.reduce((acc, el) => {
|
||||
const { width, height } = cappedElementCanvasSize(el, appState.zoom);
|
||||
return acc + width * height;
|
||||
}, 0),
|
||||
),
|
||||
);
|
||||
visibleElements.forEach((element) => {
|
||||
try {
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
@@ -426,7 +445,9 @@ export const _renderScene = ({
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
if (showDebug) {
|
||||
console.log(`finish: renderElements ${Date.now() - start}`);
|
||||
}
|
||||
if (editingLinearElement) {
|
||||
renderLinearPointHandles(
|
||||
context,
|
||||
@@ -636,10 +657,8 @@ export const _renderScene = ({
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
|
||||
// Reset zoom
|
||||
context.restore();
|
||||
|
||||
// Paint remote pointers
|
||||
for (const clientId in renderConfig.remotePointerViewportCoords) {
|
||||
let { x, y } = renderConfig.remotePointerViewportCoords[clientId];
|
||||
@@ -787,7 +806,6 @@ export const _renderScene = ({
|
||||
});
|
||||
context.restore();
|
||||
}
|
||||
|
||||
context.restore();
|
||||
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
|
||||
};
|
||||
|
||||
@@ -282,7 +282,6 @@ exports[`restoreElements should restore text element correctly passing value for
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"baseline": 0,
|
||||
"boundElements": Array [],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
@@ -322,7 +321,6 @@ exports[`restoreElements should restore text element correctly with unknown font
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"baseline": 0,
|
||||
"boundElements": Array [],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
|
||||
21
src/utils.ts
21
src/utils.ts
@@ -135,17 +135,24 @@ export const throttleRAF = <T extends any[]>(
|
||||
let timerId: number | null = null;
|
||||
let lastArgs: T | null = null;
|
||||
let lastArgsTrailing: T | null = null;
|
||||
let watchdog: number | null = null;
|
||||
|
||||
const scheduleFunc = (args: T) => {
|
||||
timerId = window.requestAnimationFrame(() => {
|
||||
timerId = null;
|
||||
//console.log("start render in animation frame");
|
||||
fn(...args);
|
||||
//console.log("render done in animation frame");
|
||||
lastArgs = null;
|
||||
if (lastArgsTrailing) {
|
||||
//console.log("last args trailing", lastArgsTrailing);
|
||||
lastArgs = lastArgsTrailing;
|
||||
lastArgsTrailing = null;
|
||||
scheduleFunc(lastArgs);
|
||||
}
|
||||
if (watchdog) {
|
||||
clearTimeout(watchdog);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -165,6 +172,9 @@ export const throttleRAF = <T extends any[]>(
|
||||
if (timerId !== null) {
|
||||
cancelAnimationFrame(timerId);
|
||||
timerId = null;
|
||||
if (watchdog) {
|
||||
clearTimeout(watchdog);
|
||||
}
|
||||
}
|
||||
if (lastArgs) {
|
||||
fn(...(lastArgsTrailing || lastArgs));
|
||||
@@ -176,8 +186,19 @@ export const throttleRAF = <T extends any[]>(
|
||||
if (timerId !== null) {
|
||||
cancelAnimationFrame(timerId);
|
||||
timerId = null;
|
||||
if (watchdog) {
|
||||
clearTimeout(watchdog);
|
||||
watchdog = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
watchdog = window.setTimeout(() => {
|
||||
console.log("watchdog", timerId);
|
||||
if (timerId !== null) {
|
||||
cancelAnimationFrame(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
}, 1000);
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user