2
0
Эх сурвалжийг харах

upload images and store them as base64

kbariotis 5 жил өмнө
parent
commit
0cb67a0bc9

+ 45 - 1
src/components/App.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 
+import { fileOpen } from "browser-nativefs";
 import socketIOClient from "socket.io-client";
 import rough from "roughjs/bin/rough";
 import { RoughCanvas } from "roughjs/bin/canvas";
@@ -7,6 +8,7 @@ import { simplify, Point } from "points-on-curve";
 import { FlooredNumber, SocketUpdateData } from "../types";
 
 import {
+  newImageElement,
   newElement,
   newTextElement,
   duplicateElement,
@@ -2058,6 +2060,30 @@ class App extends React.Component<any, AppState> {
           editingElement: element,
         });
       }
+    } else if (this.state.elementType === "image") {
+      const element = newImageElement({
+        x: x,
+        y: y,
+        strokeColor: this.state.currentItemStrokeColor,
+        backgroundColor: this.state.currentItemBackgroundColor,
+        fillStyle: this.state.currentItemFillStyle,
+        strokeWidth: this.state.currentItemStrokeWidth,
+        roughness: this.state.currentItemRoughness,
+        opacity: this.state.currentItemOpacity,
+      });
+      this.setState(() => ({
+        selectedElementIds: {
+          [element.id]: true,
+        },
+      }));
+      globalSceneState.replaceAllElements([
+        ...globalSceneState.getElementsIncludingDeleted(),
+        element,
+      ]);
+      this.setState({
+        draggingElement: element,
+        editingElement: element,
+      });
     } else {
       const element = newElement({
         type: this.state.elementType,
@@ -2313,7 +2339,7 @@ class App extends React.Component<any, AppState> {
       }
     });
 
-    const onPointerUp = withBatchedUpdates((childEvent: PointerEvent) => {
+    const onPointerUp = withBatchedUpdates(async (childEvent: PointerEvent) => {
       const {
         draggingElement,
         resizingElement,
@@ -2338,6 +2364,24 @@ class App extends React.Component<any, AppState> {
       window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
       window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
 
+      if (draggingElement?.type === "image") {
+        const selectedFile = await fileOpen({
+          description: "Image",
+          extensions: ["jpg", "jpeg", "png"],
+          mimeTypes: ["image/jpeg", "image/png"],
+        });
+
+        const reader = new FileReader();
+        reader.onload = () => {
+          mutateElement(draggingElement, {
+            imageData: reader.result as string,
+          });
+          this.actionManager.executeAction(actionFinalize);
+        };
+        reader.readAsDataURL(selectedFile);
+        return;
+      }
+
       if (draggingElement?.type === "draw") {
         this.actionManager.executeAction(actionFinalize);
         return;

+ 2 - 1
src/element/collision.ts

@@ -25,6 +25,7 @@ function isElementDraggableFromInside(
 ): boolean {
   const dragFromInside =
     element.backgroundColor !== "transparent" ||
+    element.type === "image" ||
     appState.selectedElementIds[element.id];
   if (element.type === "line" || element.type === "draw") {
     return dragFromInside && isPathALoop(element.points);
@@ -89,7 +90,7 @@ export function hitTest(
       );
     }
     return Math.hypot(a * tx - px, b * ty - py) < lineThreshold;
-  } else if (element.type === "rectangle") {
+  } else if (element.type === "rectangle" || element.type === "image") {
     if (isElementDraggableFromInside(element, appState)) {
       return (
         x > x1 - lineThreshold &&

+ 1 - 0
src/element/index.ts

@@ -9,6 +9,7 @@ export {
   newElement,
   newTextElement,
   newLinearElement,
+  newImageElement,
   duplicateElement,
 } from "./newElement";
 export {

+ 2 - 1
src/element/mutateElement.ts

@@ -19,7 +19,7 @@ export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
 ) {
   // casting to any because can't use `in` operator
   // (see https://github.com/microsoft/TypeScript/issues/21732)
-  const { points } = updates as any;
+  const { points, imageData } = updates as any;
 
   if (typeof points !== "undefined") {
     updates = { ...getSizeFromPoints(points), ...updates };
@@ -36,6 +36,7 @@ export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
   if (
     typeof updates.height !== "undefined" ||
     typeof updates.width !== "undefined" ||
+    typeof imageData !== "undefined" ||
     typeof points !== "undefined"
   ) {
     invalidateShapeForElement(element);

+ 10 - 0
src/element/newElement.ts

@@ -1,5 +1,6 @@
 import {
   ExcalidrawElement,
+  ExcalidrawImageElement,
   ExcalidrawTextElement,
   ExcalidrawLinearElement,
   ExcalidrawGenericElement,
@@ -110,6 +111,15 @@ export function newLinearElement(
   };
 }
 
+export function newImageElement(
+  opts: ElementConstructorOpts,
+): NonDeleted<ExcalidrawImageElement> {
+  return {
+    ..._newElementBase<ExcalidrawImageElement>("image", opts),
+    imageData: "",
+  };
+}
+
 // Simplified deep clone for the purpose of cloning ExcalidrawElement only
 //  (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
 //

+ 8 - 1
src/element/types.ts

@@ -31,7 +31,8 @@ export type ExcalidrawGenericElement = _ExcalidrawElementBase & {
 export type ExcalidrawElement =
   | ExcalidrawGenericElement
   | ExcalidrawTextElement
-  | ExcalidrawLinearElement;
+  | ExcalidrawLinearElement
+  | ExcalidrawImageElement;
 
 export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
   isDeleted: false;
@@ -55,6 +56,12 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
     lastCommittedPoint?: Point | null;
   }>;
 
+export type ExcalidrawImageElement = _ExcalidrawElementBase &
+  Readonly<{
+    type: "image";
+    imageData: string;
+  }>;
+
 export type PointerType = "mouse" | "pen" | "touch";
 
 export type TextAlign = "left" | "center" | "right";

+ 1 - 0
src/locales/en.json

@@ -96,6 +96,7 @@
   },
   "toolBar": {
     "selection": "Selection",
+    "image": "Image",
     "draw": "Free draw",
     "rectangle": "Rectangle",
     "diamond": "Diamond",

+ 21 - 1
src/renderer/renderElement.ts

@@ -95,6 +95,20 @@ function drawElementOnCanvas(
       );
       break;
     }
+    case "image": {
+      const img = new Image();
+      img.onload = function () {
+        context.drawImage(
+          img,
+          20 /* hardcoded for the selection box*/,
+          20,
+          element.width,
+          element.height,
+        );
+      };
+      img.src = element.imageData;
+      break;
+    }
     default: {
       if (isTextElement(element)) {
         const font = context.font;
@@ -271,6 +285,11 @@ function generateElement(
         shape = [];
         break;
       }
+      case "image": {
+        // just to ensure we don't regenerate element.canvas on rerenders
+        shape = [];
+        break;
+      }
     }
     shapeCache.set(element, shape);
   }
@@ -345,7 +364,8 @@ export function renderElement(
     case "line":
     case "draw":
     case "arrow":
-    case "text": {
+    case "text":
+    case "image": {
       const elementWithCanvas = generateElement(element, generator, sceneState);
 
       if (renderOptimizations) {

+ 13 - 0
src/shapes.tsx

@@ -93,6 +93,19 @@ export const SHAPES = [
     value: "text",
     key: "t",
   },
+  {
+    icon: (
+      // fa-image
+      <svg viewBox="0 0 512 512">
+        <path
+          fill="currentColor"
+          d="M464 64H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V112c0-26.51-21.49-48-48-48zm-6 336H54a6 6 0 0 1-6-6V118a6 6 0 0 1 6-6h404a6 6 0 0 1 6 6v276a6 6 0 0 1-6 6zM128 152c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zM96 352h320v-80l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L192 304l-39.515-39.515c-4.686-4.686-12.284-4.686-16.971 0L96 304v48z"
+        ></path>
+      </svg>
+    ),
+    value: "image",
+    key: "i",
+  },
 ] as const;
 
 export const shapesShortcutKeys = SHAPES.map((shape, index) => [