upload images and store them as base64

This commit is contained in:
kbariotis 2020-05-13 21:42:56 +01:00
parent ad81033a78
commit 0cb67a0bc9
9 changed files with 103 additions and 5 deletions

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { fileOpen } from "browser-nativefs";
import socketIOClient from "socket.io-client"; import socketIOClient from "socket.io-client";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
@ -7,6 +8,7 @@ import { simplify, Point } from "points-on-curve";
import { FlooredNumber, SocketUpdateData } from "../types"; import { FlooredNumber, SocketUpdateData } from "../types";
import { import {
newImageElement,
newElement, newElement,
newTextElement, newTextElement,
duplicateElement, duplicateElement,
@ -2058,6 +2060,30 @@ class App extends React.Component<any, AppState> {
editingElement: element, 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 { } else {
const element = newElement({ const element = newElement({
type: this.state.elementType, 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 { const {
draggingElement, draggingElement,
resizingElement, resizingElement,
@ -2338,6 +2364,24 @@ class App extends React.Component<any, AppState> {
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove); window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, onPointerUp); 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") { if (draggingElement?.type === "draw") {
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
return; return;

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawTextElement, ExcalidrawTextElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawGenericElement, 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 // Simplified deep clone for the purpose of cloning ExcalidrawElement only
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.) // (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
// //

View File

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

View File

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

View File

@ -95,6 +95,20 @@ function drawElementOnCanvas(
); );
break; 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: { default: {
if (isTextElement(element)) { if (isTextElement(element)) {
const font = context.font; const font = context.font;
@ -271,6 +285,11 @@ function generateElement(
shape = []; shape = [];
break; break;
} }
case "image": {
// just to ensure we don't regenerate element.canvas on rerenders
shape = [];
break;
}
} }
shapeCache.set(element, shape); shapeCache.set(element, shape);
} }
@ -345,7 +364,8 @@ export function renderElement(
case "line": case "line":
case "draw": case "draw":
case "arrow": case "arrow":
case "text": { case "text":
case "image": {
const elementWithCanvas = generateElement(element, generator, sceneState); const elementWithCanvas = generateElement(element, generator, sceneState);
if (renderOptimizations) { if (renderOptimizations) {

View File

@ -93,6 +93,19 @@ export const SHAPES = [
value: "text", value: "text",
key: "t", 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; ] as const;
export const shapesShortcutKeys = SHAPES.map((shape, index) => [ export const shapesShortcutKeys = SHAPES.map((shape, index) => [