Compare commits

...

1 Commits

Author SHA1 Message Date
kbariotis
0cb67a0bc9 upload images and store them as base64 2020-05-13 21:42:56 +01:00
9 changed files with 103 additions and 5 deletions

View File

@ -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;

View File

@ -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 &&

View File

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

View File

@ -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);

View File

@ -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.)
//

View File

@ -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";

View File

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

View File

@ -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) {

View File

@ -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) => [