Browse Source

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>
Daishi Kato 4 years ago
parent
commit
7edcea9a93

+ 46 - 41
src/element/handlerRectangles.ts

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

+ 105 - 0
src/element/resizeElements.ts

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

+ 0 - 5
src/element/resizeTest.ts

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

+ 1 - 1
src/renderer/renderScene.ts

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