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:
Daishi Kato 2020-05-28 07:17:15 +09:00 committed by GitHub
parent 5327e8a3dc
commit 7edcea9a93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 47 deletions

View File

@ -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,
);
};

View File

@ -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",

View File

@ -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) {

View File

@ -304,7 +304,7 @@ export const renderScene = (
handler[2],
handler[3],
);
} else if (locallySelectedElements[0].type !== "text") {
} else {
strokeRectWithRotation(
context,
handler[0],