From 0cb67a0bc9c484a5055434e853eaa6322fd39692 Mon Sep 17 00:00:00 2001 From: kbariotis Date: Wed, 13 May 2020 21:42:56 +0100 Subject: [PATCH] upload images and store them as base64 --- src/components/App.tsx | 46 ++++++++++++++++++++++++++++++++++- src/element/collision.ts | 3 ++- src/element/index.ts | 1 + src/element/mutateElement.ts | 3 ++- src/element/newElement.ts | 10 ++++++++ src/element/types.ts | 9 ++++++- src/locales/en.json | 1 + src/renderer/renderElement.ts | 22 ++++++++++++++++- src/shapes.tsx | 13 ++++++++++ 9 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index bc1aa50da..bf103752c 100644 --- a/src/components/App.tsx +++ b/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 { 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 { } }); - const onPointerUp = withBatchedUpdates((childEvent: PointerEvent) => { + const onPointerUp = withBatchedUpdates(async (childEvent: PointerEvent) => { const { draggingElement, resizingElement, @@ -2338,6 +2364,24 @@ class App extends React.Component { 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; diff --git a/src/element/collision.ts b/src/element/collision.ts index b49a5188d..7de7572ea 100644 --- a/src/element/collision.ts +++ b/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 && diff --git a/src/element/index.ts b/src/element/index.ts index 7cecd0104..e8336ea36 100644 --- a/src/element/index.ts +++ b/src/element/index.ts @@ -9,6 +9,7 @@ export { newElement, newTextElement, newLinearElement, + newImageElement, duplicateElement, } from "./newElement"; export { diff --git a/src/element/mutateElement.ts b/src/element/mutateElement.ts index 400e06ab3..d8a95bbf9 100644 --- a/src/element/mutateElement.ts +++ b/src/element/mutateElement.ts @@ -19,7 +19,7 @@ export function mutateElement>( ) { // 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>( if ( typeof updates.height !== "undefined" || typeof updates.width !== "undefined" || + typeof imageData !== "undefined" || typeof points !== "undefined" ) { invalidateShapeForElement(element); diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 81ec355cd..d2fc4272f 100644 --- a/src/element/newElement.ts +++ b/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 { + return { + ..._newElementBase("image", opts), + imageData: "", + }; +} + // Simplified deep clone for the purpose of cloning ExcalidrawElement only // (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.) // diff --git a/src/element/types.ts b/src/element/types.ts index 75c199758..3be3e2ec4 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -31,7 +31,8 @@ export type ExcalidrawGenericElement = _ExcalidrawElementBase & { export type ExcalidrawElement = | ExcalidrawGenericElement | ExcalidrawTextElement - | ExcalidrawLinearElement; + | ExcalidrawLinearElement + | ExcalidrawImageElement; export type NonDeleted = 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"; diff --git a/src/locales/en.json b/src/locales/en.json index 305d7865e..589d86a40 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -96,6 +96,7 @@ }, "toolBar": { "selection": "Selection", + "image": "Image", "draw": "Free draw", "rectangle": "Rectangle", "diamond": "Diamond", diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index ddbc8d9dc..93afaca2d 100644 --- a/src/renderer/renderElement.ts +++ b/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) { diff --git a/src/shapes.tsx b/src/shapes.tsx index 0941d01d2..b9d5eec64 100644 --- a/src/shapes.tsx +++ b/src/shapes.tsx @@ -93,6 +93,19 @@ export const SHAPES = [ value: "text", key: "t", }, + { + icon: ( + // fa-image + + + + ), + value: "image", + key: "i", + }, ] as const; export const shapesShortcutKeys = SHAPES.map((shape, index) => [