Browse Source

Fixes text jumping on creation (#266)

* Fixes text jumping on creation

* Do not remove node on ESC

* Fixed typo
Timur Khazamov 5 years ago
parent
commit
37934c0f8b
5 changed files with 52 additions and 23 deletions
  1. 6 1
      src/element/textWysiwyg.tsx
  2. 3 1
      src/element/types.ts
  3. 12 20
      src/index.tsx
  4. 4 1
      src/renderer/renderElement.ts
  5. 27 0
      src/utils.ts

+ 6 - 1
src/element/textWysiwyg.tsx

@@ -22,7 +22,7 @@ export function textWysiwyg({
   Object.assign(input.style, {
     color: strokeColor,
     position: "absolute",
-    top: y - 8 + "px",
+    top: y + "px",
     left: x + "px",
     transform: "translate(-50%, -50%)",
     boxShadow: "none",
@@ -36,6 +36,11 @@ export function textWysiwyg({
   input.onkeydown = ev => {
     if (ev.key === KEYS.ESCAPE) {
       ev.preventDefault();
+      if (initText) {
+        input.value = initText;
+        handleSubmit();
+        return;
+      }
       cleanup();
       return;
     }

+ 3 - 1
src/element/types.ts

@@ -5,5 +5,7 @@ export type ExcalidrawTextElement = ExcalidrawElement & {
   type: "text";
   font: string;
   text: string;
-  actualBoundingBoxAscent: number;
+  // for backward compatibility
+  actualBoundingBoxAscent?: number;
+  baseline: number;
 };

+ 12 - 20
src/index.tsx

@@ -34,13 +34,12 @@ import { renderScene } from "./renderer";
 import { AppState } from "./types";
 import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
 
-import { getDateTime, isInputLike } from "./utils";
+import { getDateTime, isInputLike, measureText } from "./utils";
 
 import { ButtonSelect } from "./components/ButtonSelect";
 import { findShapeByKey, shapesShortcutKeys } from "./shapes";
 import { createHistory } from "./history";
 
-import "./styles.scss";
 import ContextMenu from "./components/ContextMenu";
 import { PanelTools } from "./components/panels/PanelTools";
 import { PanelSelection } from "./components/panels/PanelSelection";
@@ -48,6 +47,8 @@ import { PanelColor } from "./components/panels/PanelColor";
 import { PanelExport } from "./components/panels/PanelExport";
 import { PanelCanvas } from "./components/panels/PanelCanvas";
 
+import "./styles.scss";
+
 const { elements } = createScene();
 const { history } = createHistory();
 const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
@@ -94,23 +95,16 @@ function addTextElement(
   if (text === null || text === "") {
     return false;
   }
+
+  const metrics = measureText(text, font);
   element.text = text;
   element.font = font;
-  const currentFont = context.font;
-  context.font = element.font;
-  const textMeasure = context.measureText(element.text);
-  const width = textMeasure.width;
-  const actualBoundingBoxAscent =
-    textMeasure.actualBoundingBoxAscent || parseInt(font);
-  const actualBoundingBoxDescent = textMeasure.actualBoundingBoxDescent || 0;
-  element.actualBoundingBoxAscent = actualBoundingBoxAscent;
-  context.font = currentFont;
-  const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
   // Center the text
-  element.x -= width / 2;
-  element.y -= actualBoundingBoxAscent;
-  element.width = width;
-  element.height = height;
+  element.x -= metrics.width / 2;
+  element.y -= metrics.height / 2;
+  element.width = metrics.width;
+  element.height = metrics.height;
+  element.baseline = metrics.baseline;
 
   return true;
 }
@@ -965,8 +959,7 @@ class App extends React.Component<{}, AppState> {
               Object.assign(element, elementAtPosition);
               // x and y will change after calling addTextElement function
               element.x = elementAtPosition.x + elementAtPosition.width / 2;
-              element.y =
-                elementAtPosition.y + elementAtPosition.actualBoundingBoxAscent;
+              element.y = elementAtPosition.y + elementAtPosition.height / 2;
               initText = elementAtPosition.text;
               textX =
                 this.state.scrollX +
@@ -977,7 +970,7 @@ class App extends React.Component<{}, AppState> {
                 this.state.scrollY +
                 elementAtPosition.y +
                 CANVAS_WINDOW_OFFSET_TOP +
-                elementAtPosition.actualBoundingBoxAscent;
+                elementAtPosition.height / 2;
             }
 
             textWysiwyg({
@@ -1063,6 +1056,5 @@ const rootElement = document.getElementById("root");
 ReactDOM.render(<App />, rootElement);
 const canvas = document.getElementById("canvas") as HTMLCanvasElement;
 const rc = rough.canvas(canvas);
-const context = canvas.getContext("2d")!;
 
 ReactDOM.render(<App />, rootElement);

+ 4 - 1
src/renderer/renderElement.ts

@@ -131,11 +131,14 @@ export function renderElement(
     context.fillText(
       element.text,
       element.x + scrollX,
-      element.y + element.actualBoundingBoxAscent + scrollY
+      element.y +
+        scrollY +
+        (element.baseline || element.actualBoundingBoxAscent || 0)
     );
     context.fillStyle = fillStyle;
     context.font = font;
     context.globalAlpha = 1;
+    console.log(element);
   } else {
     throw new Error("Unimplemented type " + element.type);
   }

+ 27 - 0
src/utils.ts

@@ -23,3 +23,30 @@ export function isInputLike(
     target instanceof HTMLSelectElement
   );
 }
+
+// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
+export function measureText(text: string, font: string) {
+  const line = document.createElement("div");
+  const body = document.body;
+  line.style.position = "absolute";
+  line.style.whiteSpace = "nowrap";
+  line.style.font = font;
+  body.appendChild(line);
+  // Now we can measure width and height of the letter
+  line.innerHTML = text;
+  const width = line.offsetWidth;
+  const height = line.offsetHeight;
+  // Now creating 1px sized item that will be aligned to baseline
+  // to calculate baseline shift
+  const span = document.createElement("span");
+  span.style.display = "inline-block";
+  span.style.overflow = "hidden";
+  span.style.width = "1px";
+  span.style.height = "1px";
+  line.appendChild(span);
+  // Baseline is important for positioning text on canvas
+  const baseline = span.offsetTop + span.offsetHeight;
+  document.body.removeChild(line);
+
+  return { width, height, baseline };
+}