Compare commits

..
Author SHA1 Message Date
dwelle 0f32b7c1ef wip: adaptive roughness 2023-04-09 19:52:18 +02:00
dwelle b885d87bc5 feat: remove notch for non-0 roughness ellipse 2023-04-09 18:52:36 +02:00
Manuel BrandstetterandGitHub c170403b13 docs: fix closing tag in readme (#6424
Update README.md
2023-04-07 17:43:50 +02:00
705ac9c1ab fix: element vanishes when zoomed in (#6417)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-04-07 15:30:44 +00:00
68692b9d4c feat: add container to multiple text elements (#6428)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-07 13:50:36 +00:00
20 changed files with 267 additions and 316 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
An open source virtual hand-drawn style whiteboard. </br>
Collaborative and end-to-end encrypted. </br>
<br />
</h3>
</h2>
</div>
<br />
+1
View File
@@ -20,6 +20,7 @@
},
"dependencies": {
"@dwelle/tunnel-rat": "0.1.1",
"@excalidraw/roughjs": "4.5.3",
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2",
+1 -1
View File
@@ -94,7 +94,7 @@
// if its present redirect
document.cookie.includes("excplus-autoredirect=true")
) {
window.location.href = "https://app.excalidraw.com";
// window.location.href = "https://app.excalidraw.com";
}
</script>
+91 -88
View File
@@ -23,6 +23,7 @@ import {
ExcalidrawTextElement,
} from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { getFontString } from "../utils";
import { register } from "./register";
@@ -42,7 +43,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 +57,6 @@ export const actionUnbindText = register({
containerId: null,
width,
height,
baseline,
text: boundTextElement.originalText,
});
mutateElement(element, {
@@ -186,107 +186,110 @@ export const actionCreateContainerFromText = register({
trackEvent: { category: "element" },
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1 && isTextElement(selectedElements[0]);
const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements;
},
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const updatedElements = elements.slice();
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
const textElement = selectedElements[0];
const container = newElement({
type: "rectangle",
backgroundColor: appState.currentItemBackgroundColor,
boundElements: [
...(textElement.boundElements || []),
{ id: textElement.id, type: "text" },
],
angle: textElement.angle,
fillStyle: appState.currentItemFillStyle,
strokeColor: appState.currentItemStrokeColor,
roughness: appState.currentItemRoughness,
strokeWidth: appState.currentItemStrokeWidth,
strokeStyle: appState.currentItemStrokeStyle,
roundness:
appState.currentItemRoundness === "round"
? {
type: isUsingAdaptiveRadius("rectangle")
? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS,
}
: null,
opacity: 100,
locked: false,
x: textElement.x - BOUND_TEXT_PADDING,
y: textElement.y - BOUND_TEXT_PADDING,
width: computeContainerDimensionForBoundText(
textElement.width,
"rectangle",
),
height: computeContainerDimensionForBoundText(
textElement.height,
"rectangle",
),
groupIds: textElement.groupIds,
});
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: AppState["selectedElementIds"] = {};
// update bindings
if (textElement.boundElements?.length) {
const linearElementIds = textElement.boundElements
.filter((ele) => ele.type === "arrow")
.map((el) => el.id);
const linearElements = updatedElements.filter((ele) =>
linearElementIds.includes(ele.id),
) as ExcalidrawLinearElement[];
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
if (startBinding?.elementId === textElement.id) {
startBinding = {
...startBinding,
elementId: container.id,
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding });
}
for (const textElement of selectedElements) {
if (isTextElement(textElement)) {
const container = newElement({
type: "rectangle",
backgroundColor: appState.currentItemBackgroundColor,
boundElements: [
...(textElement.boundElements || []),
{ id: textElement.id, type: "text" },
],
angle: textElement.angle,
fillStyle: appState.currentItemFillStyle,
strokeColor: appState.currentItemStrokeColor,
roughness: appState.currentItemRoughness,
strokeWidth: appState.currentItemStrokeWidth,
strokeStyle: appState.currentItemStrokeStyle,
roundness:
appState.currentItemRoundness === "round"
? {
type: isUsingAdaptiveRadius("rectangle")
? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS,
}
: null,
opacity: 100,
locked: false,
x: textElement.x - BOUND_TEXT_PADDING,
y: textElement.y - BOUND_TEXT_PADDING,
width: computeContainerDimensionForBoundText(
textElement.width,
"rectangle",
),
height: computeContainerDimensionForBoundText(
textElement.height,
"rectangle",
),
groupIds: textElement.groupIds,
});
}
mutateElement(textElement, {
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
});
redrawTextBoundingBox(textElement, container);
// update bindings
if (textElement.boundElements?.length) {
const linearElementIds = textElement.boundElements
.filter((ele) => ele.type === "arrow")
.map((el) => el.id);
const linearElements = updatedElements.filter((ele) =>
linearElementIds.includes(ele.id),
) as ExcalidrawLinearElement[];
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
return {
elements: pushContainerBelowText(
[...elements, container],
if (startBinding?.elementId === textElement.id) {
startBinding = {
...startBinding,
elementId: container.id,
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding }, false);
}
});
}
mutateElement(
textElement,
{
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
},
false,
);
redrawTextBoundingBox(textElement, container);
updatedElements = pushContainerBelowText(
[...updatedElements, container],
container,
textElement,
),
appState: {
...appState,
selectedElementIds: {
[container.id]: true,
[textElement.id]: false,
},
},
commitToHistory: true,
};
);
containerIds[container.id] = true;
}
}
return {
elements: updatedElements,
appState,
appState: {
...appState,
selectedElementIds: containerIds,
},
commitToHistory: true,
};
},
+2 -2
View File
@@ -1,8 +1,8 @@
import React, { useContext } from "react";
import { flushSync } from "react-dom";
import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import { RoughCanvas } from "@excalidraw/roughjs/bin/canvas";
import rough from "@excalidraw/roughjs/bin/rough";
import clsx from "clsx";
import { nanoid } from "nanoid";
+15 -27
View File
@@ -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) {
+2 -2
View File
@@ -7,8 +7,8 @@ import {
ExcalidrawTextElementWithContainer,
} from "./types";
import { distance2d, rotate } from "../math";
import rough from "roughjs/bin/rough";
import { Drawable, Op } from "roughjs/bin/core";
import rough from "@excalidraw/roughjs/bin/rough";
import { Drawable, Op } from "@excalidraw/roughjs/bin/core";
import { Point } from "../types";
import {
getShapeForElement,
+1 -1
View File
@@ -30,7 +30,7 @@ import {
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
import { Point } from "../types";
import { Drawable } from "roughjs/bin/core";
import { Drawable } from "@excalidraw/roughjs/bin/core";
import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement";
import { hasBoundTextElement, isImageElement } from "./typeChecks";
+5 -9
View File
@@ -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,
};
+18 -43
View File
@@ -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,
};
}
}
+5 -65
View File
@@ -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 -27
View File
@@ -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,
-1
View File
@@ -131,7 +131,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
fontSize: number;
fontFamily: FontFamilyValues;
text: string;
baseline: number;
textAlign: TextAlign;
verticalAlign: VerticalAlign;
containerId: ExcalidrawGenericElement["id"] | null;
+1 -1
View File
@@ -1,4 +1,4 @@
import { Random } from "roughjs/bin/math";
import { Random } from "@excalidraw/roughjs/bin/math";
import { nanoid } from "nanoid";
import { isTestEnv } from "./utils";
+100 -42
View File
@@ -21,15 +21,15 @@ import {
getElementAbsoluteCoords,
getArrowheadPoints,
} from "../element/bounds";
import { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable, Options } from "roughjs/bin/core";
import { RoughSVG } from "roughjs/bin/svg";
import { RoughGenerator } from "roughjs/bin/generator";
import { RoughCanvas } from "@excalidraw/roughjs/bin/canvas";
import { Drawable, Options } from "@excalidraw/roughjs/bin/core";
import { RoughSVG } from "@excalidraw/roughjs/bin/svg";
import { RoughGenerator } from "@excalidraw/roughjs/bin/generator";
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 rough from "@excalidraw/roughjs/bin/rough";
import { AppState, BinaryFiles, Zoom } from "../types";
import { getDefaultAppState } from "../appState";
import {
@@ -87,12 +87,66 @@ export interface ExcalidrawElementWithCanvas {
element: ExcalidrawElement | ExcalidrawTextElement;
canvas: HTMLCanvasElement;
theme: RenderConfig["theme"];
canvasZoom: Zoom["value"];
scale: number;
canvasOffsetX: number;
canvasOffsetY: number;
boundTextElementVersion: number | null;
}
const cappedElementCanvasSize = (
element: NonDeletedExcalidrawElement,
zoom: Zoom,
): {
width: number;
height: number;
scale: number;
} => {
// these limits are ballpark, they depend on specific browsers and device.
// We've chosen lower limits to be safe. We might want to change these limits
// based on browser/device type, if we get reports of low quality rendering
// on zoom.
//
// ~ safari mobile canvas area limit
const AREA_LIMIT = 16777216;
// ~ safari width/height limit based on developer.mozilla.org.
const WIDTH_HEIGHT_LIMIT = 32767;
const padding = getCanvasPadding(element);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const elementWidth =
isLinearElement(element) || isFreeDrawElement(element)
? distance(x1, x2)
: element.width;
const elementHeight =
isLinearElement(element) || isFreeDrawElement(element)
? distance(y1, y2)
: element.height;
let width = elementWidth * window.devicePixelRatio + padding * 2;
let height = elementHeight * window.devicePixelRatio + padding * 2;
let scale: number = zoom.value;
// rescale to ensure width and height is within limits
if (
width * scale > WIDTH_HEIGHT_LIMIT ||
height * scale > WIDTH_HEIGHT_LIMIT
) {
scale = Math.min(WIDTH_HEIGHT_LIMIT / width, WIDTH_HEIGHT_LIMIT / height);
}
// rescale to ensure canvas area is within limits
if (width * height * scale * scale > AREA_LIMIT) {
scale = Math.sqrt(AREA_LIMIT / (width * height));
}
width = Math.floor(width * scale);
height = Math.floor(height * scale);
return { width, height, scale };
};
const generateElementCanvas = (
element: NonDeletedExcalidrawElement,
zoom: Zoom,
@@ -102,44 +156,35 @@ const generateElementCanvas = (
const context = canvas.getContext("2d")!;
const padding = getCanvasPadding(element);
const { width, height, scale } = 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 * scale
: 0;
canvasOffsetY =
element.y > y1
? distance(element.y, y1) * window.devicePixelRatio * zoom.value
? distance(element.y, y1) * window.devicePixelRatio * scale
: 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 * scale, padding * scale);
context.scale(
window.devicePixelRatio * zoom.value,
window.devicePixelRatio * zoom.value,
window.devicePixelRatio * scale,
window.devicePixelRatio * scale,
);
const rc = rough.canvas(canvas);
@@ -156,7 +201,7 @@ const generateElementCanvas = (
element,
canvas,
theme: renderConfig.theme,
canvasZoom: zoom.value,
scale,
canvasOffsetX,
canvasOffsetY,
boundTextElementVersion: getBoundTextElement(element)?.version || null,
@@ -200,6 +245,7 @@ const drawImagePlaceholder = (
size,
);
};
const drawElementOnCanvas = (
element: NonDeletedExcalidrawElement,
rc: RoughCanvas,
@@ -272,10 +318,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 +331,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();
@@ -344,6 +389,16 @@ export const setShapeForElement = <T extends ExcalidrawElement>(
export const invalidateShapeForElement = (element: ExcalidrawElement) =>
shapeCache.delete(element);
function adjustRoughness(size: number, roughness: number): number {
if (size >= 50) {
return roughness;
}
const factor = 2 + (50 - size) / 10;
// console.log({ ret: roughness / factor, roughness, factor, size });
return roughness / factor;
}
export const generateRoughOptions = (
element: ExcalidrawElement,
continuousPath = false,
@@ -370,7 +425,10 @@ export const generateRoughOptions = (
// calculate them (and we don't want the fills to be modified)
fillWeight: element.strokeWidth / 2,
hachureGap: element.strokeWidth * 4,
roughness: element.roughness,
roughness: adjustRoughness(
Math.min(element.width, element.height),
element.roughness,
),
stroke: element.strokeColor,
preserveVertices: continuousPath,
};
@@ -670,7 +728,7 @@ const generateElementWithCanvas = (
const prevElementWithCanvas = elementWithCanvasCache.get(element);
const shouldRegenerateBecauseZoom =
prevElementWithCanvas &&
prevElementWithCanvas.canvasZoom !== zoom.value &&
prevElementWithCanvas.scale !== zoom.value &&
!renderConfig?.shouldCacheIgnoreZoom;
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
@@ -701,7 +759,7 @@ const drawElementFromCanvas = (
) => {
const element = elementWithCanvas.element;
const padding = getCanvasPadding(element);
const zoom = elementWithCanvas.canvasZoom;
const zoom = elementWithCanvas.scale;
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
// Free draw elements will otherwise "shuffle" as the min x and y change
@@ -728,10 +786,10 @@ const drawElementFromCanvas = (
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
tempCanvas.width =
maxDim * window.devicePixelRatio * zoom +
padding * elementWithCanvas.canvasZoom * 10;
padding * elementWithCanvas.scale * 10;
tempCanvas.height =
maxDim * window.devicePixelRatio * zoom +
padding * elementWithCanvas.canvasZoom * 10;
padding * elementWithCanvas.scale * 10;
const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
@@ -812,11 +870,11 @@ const drawElementFromCanvas = (
context.drawImage(
elementWithCanvas.canvas!,
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
);
if (
+2 -2
View File
@@ -1,5 +1,5 @@
import { RoughCanvas } from "roughjs/bin/canvas";
import { RoughSVG } from "roughjs/bin/svg";
import { RoughCanvas } from "@excalidraw/roughjs/bin/canvas";
import { RoughSVG } from "@excalidraw/roughjs/bin/svg";
import oc from "open-color";
import { AppState, BinaryFiles, Point, Zoom } from "../types";
+1 -1
View File
@@ -1,4 +1,4 @@
import rough from "roughjs/bin/rough";
import rough from "@excalidraw/roughjs/bin/rough";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element/bounds";
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
@@ -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",
+1 -1
View File
@@ -16,7 +16,7 @@ import {
StrokeRoundness,
} from "./element/types";
import { SHAPES } from "./shapes";
import { Point as RoughPoint } from "roughjs/bin/geometry";
import { Point as RoughPoint } from "@excalidraw/roughjs/bin/geometry";
import { LinearElementEditor } from "./element/linearElementEditor";
import { SuggestedBinding } from "./element/binding";
import { ImportedDataState } from "./data/types";
+9
View File
@@ -1484,6 +1484,15 @@
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
integrity sha512-rFIq8+A8WvkEzBsF++Rw6gzxE+hU3ZNkdg8foI+Upz2y/rOC/gUpWJaggPbCkoH3nlREVU59axQjZ1+F6ePRGg==
"@excalidraw/roughjs@4.5.3":
version "4.5.3"
resolved "https://registry.yarnpkg.com/@excalidraw/roughjs/-/roughjs-4.5.3.tgz#7d91897bfd1aaeb19a48d5272bd4776cb19f015b"
integrity sha512-X9HPfNjPLNHt+gN7t1LN7nN4h4o8M2EWuN/Y4YgG0ODsDZn51gRBeIPuhSo2hsDgLeTQHnkrmpPTm4XeeQVtoA==
dependencies:
path-data-parser "^0.1.0"
points-on-curve "^0.2.0"
points-on-path "^0.2.1"
"@firebase/analytics-types@0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz#d6716f9fa36a6e340bc0ecfe68af325aa6f60508"