upload images and store them as base64
This commit is contained in:
parent
ad81033a78
commit
0cb67a0bc9
@ -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;
|
||||||
|
@ -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 &&
|
||||||
|
@ -9,6 +9,7 @@ export {
|
|||||||
newElement,
|
newElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
|
newImageElement,
|
||||||
duplicateElement,
|
duplicateElement,
|
||||||
} from "./newElement";
|
} from "./newElement";
|
||||||
export {
|
export {
|
||||||
|
@ -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);
|
||||||
|
@ -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.)
|
||||||
//
|
//
|
||||||
|
@ -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";
|
||||||
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
@ -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) => [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user