feat: resize text element (#1650)
* feat: resize text element * ignore small font size change that leads jankiness Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
5327e8a3dc
commit
7edcea9a93
@ -21,6 +21,33 @@ export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
|
||||
rotation: true,
|
||||
};
|
||||
|
||||
const OMIT_SIDES_FOR_TEXT_ELEMENT = {
|
||||
e: true,
|
||||
s: true,
|
||||
n: true,
|
||||
w: true,
|
||||
};
|
||||
|
||||
const OMIT_SIDES_FOR_LINE_SLASH = {
|
||||
e: true,
|
||||
s: true,
|
||||
n: true,
|
||||
w: true,
|
||||
nw: true,
|
||||
se: true,
|
||||
rotation: true,
|
||||
};
|
||||
|
||||
const OMIT_SIDES_FOR_LINE_BACKSLASH = {
|
||||
e: true,
|
||||
s: true,
|
||||
n: true,
|
||||
w: true,
|
||||
ne: true,
|
||||
sw: true,
|
||||
rotation: true,
|
||||
};
|
||||
|
||||
const generateHandler = (
|
||||
x: number,
|
||||
y: number,
|
||||
@ -180,13 +207,7 @@ export const handlerRectangles = (
|
||||
zoom: number,
|
||||
pointerType: PointerType = "mouse",
|
||||
) => {
|
||||
const handlers = handlerRectanglesFromCoords(
|
||||
getElementAbsoluteCoords(element),
|
||||
element.angle,
|
||||
zoom,
|
||||
pointerType,
|
||||
);
|
||||
|
||||
let omitSides: { [T in Sides]?: boolean } = {};
|
||||
if (
|
||||
element.type === "arrow" ||
|
||||
element.type === "line" ||
|
||||
@ -195,43 +216,27 @@ export const handlerRectangles = (
|
||||
if (element.points.length === 2) {
|
||||
// only check the last point because starting point is always (0,0)
|
||||
const [, p1] = element.points;
|
||||
|
||||
if (p1[0] === 0 || p1[1] === 0) {
|
||||
return {
|
||||
nw: handlers.nw,
|
||||
se: handlers.se,
|
||||
} as typeof handlers;
|
||||
}
|
||||
|
||||
if (p1[0] > 0 && p1[1] < 0) {
|
||||
return {
|
||||
ne: handlers.ne,
|
||||
sw: handlers.sw,
|
||||
} as typeof handlers;
|
||||
}
|
||||
|
||||
if (p1[0] > 0 && p1[1] > 0) {
|
||||
return {
|
||||
nw: handlers.nw,
|
||||
se: handlers.se,
|
||||
} as typeof handlers;
|
||||
}
|
||||
|
||||
if (p1[0] < 0 && p1[1] > 0) {
|
||||
return {
|
||||
ne: handlers.ne,
|
||||
sw: handlers.sw,
|
||||
} as typeof handlers;
|
||||
}
|
||||
|
||||
if (p1[0] < 0 && p1[1] < 0) {
|
||||
return {
|
||||
nw: handlers.nw,
|
||||
se: handlers.se,
|
||||
} as typeof handlers;
|
||||
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
||||
} else if (p1[0] > 0 && p1[1] < 0) {
|
||||
omitSides = OMIT_SIDES_FOR_LINE_SLASH;
|
||||
} else if (p1[0] > 0 && p1[1] > 0) {
|
||||
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
||||
} else if (p1[0] < 0 && p1[1] > 0) {
|
||||
omitSides = OMIT_SIDES_FOR_LINE_SLASH;
|
||||
} else if (p1[0] < 0 && p1[1] < 0) {
|
||||
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
||||
}
|
||||
}
|
||||
} else if (element.type === "text") {
|
||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
||||
}
|
||||
|
||||
return handlers;
|
||||
return handlerRectanglesFromCoords(
|
||||
getElementAbsoluteCoords(element),
|
||||
element.angle,
|
||||
zoom,
|
||||
pointerType,
|
||||
omitSides,
|
||||
);
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import { rescalePoints } from "../points";
|
||||
import { rotate, adjustXYWithRotation, getFlipAdjustment } from "../math";
|
||||
import {
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
@ -24,6 +25,7 @@ import {
|
||||
getResizeCenterPointKey,
|
||||
getResizeWithSidesSameLengthKey,
|
||||
} from "../keys";
|
||||
import { measureText, getFontString } from "../utils";
|
||||
|
||||
type ResizeTestType = ReturnType<typeof resizeTest>;
|
||||
|
||||
@ -55,6 +57,20 @@ export const resizeElements = (
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
} else if (
|
||||
element.type === "text" &&
|
||||
(resizeHandle === "nw" ||
|
||||
resizeHandle === "ne" ||
|
||||
resizeHandle === "sw" ||
|
||||
resizeHandle === "se")
|
||||
) {
|
||||
resizeSingleTextElement(
|
||||
element,
|
||||
resizeHandle,
|
||||
getResizeCenterPointKey(event),
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
} else if (resizeHandle) {
|
||||
resizeSingleElement(
|
||||
element,
|
||||
@ -188,6 +204,95 @@ const rescalePointsInElement = (
|
||||
}
|
||||
: {};
|
||||
|
||||
const resizeSingleTextElement = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
resizeHandle: "nw" | "ne" | "sw" | "se",
|
||||
isResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
// rotation pointer with reverse angle
|
||||
const [rotatedX, rotatedY] = rotate(
|
||||
pointerX,
|
||||
pointerY,
|
||||
cx,
|
||||
cy,
|
||||
-element.angle,
|
||||
);
|
||||
let scale;
|
||||
switch (resizeHandle) {
|
||||
case "se":
|
||||
scale = Math.max(
|
||||
(rotatedX - x1) / (x2 - x1),
|
||||
(rotatedY - y1) / (y2 - y1),
|
||||
);
|
||||
break;
|
||||
case "nw":
|
||||
scale = Math.max(
|
||||
(x2 - rotatedX) / (x2 - x1),
|
||||
(y2 - rotatedY) / (y2 - y1),
|
||||
);
|
||||
break;
|
||||
case "ne":
|
||||
scale = Math.max(
|
||||
(rotatedX - x1) / (x2 - x1),
|
||||
(y2 - rotatedY) / (y2 - y1),
|
||||
);
|
||||
break;
|
||||
case "sw":
|
||||
scale = Math.max(
|
||||
(x2 - rotatedX) / (x2 - x1),
|
||||
(rotatedY - y1) / (y2 - y1),
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (scale > 0) {
|
||||
const newFontSize = Math.max(element.fontSize * scale, 10);
|
||||
const metrics = measureText(
|
||||
element.text,
|
||||
getFontString({ fontSize: newFontSize, fontFamily: element.fontFamily }),
|
||||
);
|
||||
if (
|
||||
Math.abs(metrics.width - element.width) <= 1 ||
|
||||
Math.abs(metrics.height - element.height) <= 1
|
||||
) {
|
||||
// we ignore 1px change to avoid janky behavior
|
||||
return;
|
||||
}
|
||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||
element,
|
||||
metrics.width,
|
||||
metrics.height,
|
||||
);
|
||||
const deltaX1 = (x1 - nextX1) / 2;
|
||||
const deltaY1 = (y1 - nextY1) / 2;
|
||||
const deltaX2 = (x2 - nextX2) / 2;
|
||||
const deltaY2 = (y2 - nextY2) / 2;
|
||||
const [nextElementX, nextElementY] = adjustXYWithRotation(
|
||||
resizeHandle,
|
||||
element.x,
|
||||
element.y,
|
||||
element.angle,
|
||||
deltaX1,
|
||||
deltaY1,
|
||||
deltaX2,
|
||||
deltaY2,
|
||||
isResizeFromCenter,
|
||||
);
|
||||
mutateElement(element, {
|
||||
fontSize: newFontSize,
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
baseline: metrics.baseline,
|
||||
x: nextElementX,
|
||||
y: nextElementY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resizeSingleElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
resizeHandle: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
|
||||
|
@ -45,11 +45,6 @@ export const resizeTest = (
|
||||
return "rotation" as HandlerRectanglesRet;
|
||||
}
|
||||
|
||||
if (element.type === "text") {
|
||||
// can't resize text elements
|
||||
return false;
|
||||
}
|
||||
|
||||
const filter = Object.keys(handlers).filter((key) => {
|
||||
const handler = handlers[key as Exclude<HandlerRectanglesRet, "rotation">]!;
|
||||
if (!handler) {
|
||||
|
@ -304,7 +304,7 @@ export const renderScene = (
|
||||
handler[2],
|
||||
handler[3],
|
||||
);
|
||||
} else if (locallySelectedElements[0].type !== "text") {
|
||||
} else {
|
||||
strokeRectWithRotation(
|
||||
context,
|
||||
handler[0],
|
||||
|
Loading…
x
Reference in New Issue
Block a user