merge update
This commit is contained in:
commit
0b61083ac9
29
package-lock.json
generated
29
package-lock.json
generated
@ -1000,35 +1000,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
|
||||||
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
|
||||||
},
|
},
|
||||||
"@fortawesome/fontawesome-common-types": {
|
|
||||||
"version": "0.2.26",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.26.tgz",
|
|
||||||
"integrity": "sha512-CcM/fIFwZlRdiWG/25xE/wHbtyUuCtqoCTrr6BsWw7hH072fR++n4L56KPydAr3ANgMJMjT8v83ZFIsDc7kE+A=="
|
|
||||||
},
|
|
||||||
"@fortawesome/fontawesome-svg-core": {
|
|
||||||
"version": "1.2.26",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.26.tgz",
|
|
||||||
"integrity": "sha512-3Dfd/v2IztP1TxKOxZiB5+4kaOZK9mNy0KU1vVK7nFlPWz3gzxrCWB+AloQhQUoJ8HhGqbzjliK89Vl7PExGbw==",
|
|
||||||
"requires": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "^0.2.26"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@fortawesome/free-solid-svg-icons": {
|
|
||||||
"version": "5.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.12.0.tgz",
|
|
||||||
"integrity": "sha512-CnpsWs6GhTs9ekNB3d8rcO5HYqRkXbYKf2YNiAlTWbj5eVlPqsd/XH1F9If8jkcR1aegryAbln/qYeKVZzpM0g==",
|
|
||||||
"requires": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "^0.2.26"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@fortawesome/react-fontawesome": {
|
|
||||||
"version": "0.1.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.8.tgz",
|
|
||||||
"integrity": "sha512-I5h9YQg/ePA3Br9ISS18fcwOYmzQYDSM1ftH03/8nHkiqIVHtUyQBw482+60dnzvlr82gHt3mGm+nDUp159FCw==",
|
|
||||||
"requires": {
|
|
||||||
"prop-types": "^15.5.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@hapi/address": {
|
"@hapi/address": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
|
||||||
|
@ -6,10 +6,6 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
|
||||||
"lodash": "4.17.15",
|
|
||||||
"react": "16.12.0",
|
"react": "16.12.0",
|
||||||
"react-dom": "16.12.0",
|
"react-dom": "16.12.0",
|
||||||
"react-scripts": "3.3.0",
|
"react-scripts": "3.3.0",
|
||||||
|
@ -7,12 +7,8 @@
|
|||||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is added to the
|
<link rel="icon" href="%PUBLIC_URL%/logo.png" />
|
||||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="https://uploads.codesandbox.io/uploads/user/ed077012-e728-4a42-8395-cbd299149d62/AflB-FG_Virgil.ttf"
|
href="https://uploads.codesandbox.io/uploads/user/ed077012-e728-4a42-8395-cbd299149d62/AflB-FG_Virgil.ttf"
|
||||||
@ -20,15 +16,6 @@
|
|||||||
type="font/ttf"
|
type="font/ttf"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>Excalidraw</title>
|
<title>Excalidraw</title>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
|
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
308
src/index.tsx
308
src/index.tsx
@ -2,14 +2,6 @@ import React from "react";
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import rough from "roughjs/bin/wrappers/rough";
|
import rough from "roughjs/bin/wrappers/rough";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import {
|
|
||||||
faMousePointer,
|
|
||||||
faSquare,
|
|
||||||
faCircle,
|
|
||||||
faLongArrowAltRight,
|
|
||||||
faFont
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
|
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
|
||||||
|
|
||||||
@ -351,10 +343,23 @@ function handlerRectangles(
|
|||||||
|
|
||||||
function renderScene(
|
function renderScene(
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
canvas: HTMLCanvasElement,
|
||||||
sceneState: SceneState
|
sceneState: SceneState,
|
||||||
|
// extra options, currently passed by export helper
|
||||||
|
{
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
renderScrollbars = true,
|
||||||
|
renderSelection = true
|
||||||
|
}: {
|
||||||
|
offsetX?: number;
|
||||||
|
offsetY?: number;
|
||||||
|
renderScrollbars?: boolean;
|
||||||
|
renderSelection?: boolean;
|
||||||
|
} = {}
|
||||||
) {
|
) {
|
||||||
if (!context) return;
|
if (!canvas) return;
|
||||||
|
const context = canvas.getContext("2d")!;
|
||||||
|
|
||||||
const fillStyle = context.fillStyle;
|
const fillStyle = context.fillStyle;
|
||||||
if (typeof sceneState.viewBackgroundColor === "string") {
|
if (typeof sceneState.viewBackgroundColor === "string") {
|
||||||
@ -367,9 +372,15 @@ function renderScene(
|
|||||||
|
|
||||||
const selectedIndices = getSelectedIndices();
|
const selectedIndices = getSelectedIndices();
|
||||||
|
|
||||||
|
sceneState = {
|
||||||
|
...sceneState,
|
||||||
|
scrollX: typeof offsetX === "number" ? offsetX : sceneState.scrollX,
|
||||||
|
scrollY: typeof offsetY === "number" ? offsetY : sceneState.scrollY
|
||||||
|
};
|
||||||
|
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
element.draw(rc, context, sceneState);
|
element.draw(rc, context, sceneState);
|
||||||
if (element.isSelected) {
|
if (renderSelection && element.isSelected) {
|
||||||
const margin = 4;
|
const margin = 4;
|
||||||
|
|
||||||
const elementX1 = getElementAbsoluteX1(element);
|
const elementX1 = getElementAbsoluteX1(element);
|
||||||
@ -405,119 +416,93 @@ function renderScene(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrollBars = getScrollbars(
|
if (renderScrollbars) {
|
||||||
context.canvas.width,
|
const scrollBars = getScrollbars(
|
||||||
context.canvas.height,
|
context.canvas.width,
|
||||||
sceneState.scrollX,
|
context.canvas.height,
|
||||||
sceneState.scrollY
|
sceneState.scrollX,
|
||||||
);
|
sceneState.scrollY
|
||||||
|
);
|
||||||
|
|
||||||
context.fillStyle = SCROLLBAR_COLOR;
|
context.fillStyle = SCROLLBAR_COLOR;
|
||||||
context.fillRect(
|
context.fillRect(
|
||||||
scrollBars.horizontal.x,
|
scrollBars.horizontal.x,
|
||||||
scrollBars.horizontal.y,
|
scrollBars.horizontal.y,
|
||||||
scrollBars.horizontal.width,
|
scrollBars.horizontal.width,
|
||||||
scrollBars.horizontal.height
|
scrollBars.horizontal.height
|
||||||
);
|
);
|
||||||
context.fillRect(
|
context.fillRect(
|
||||||
scrollBars.vertical.x,
|
scrollBars.vertical.x,
|
||||||
scrollBars.vertical.y,
|
scrollBars.vertical.y,
|
||||||
scrollBars.vertical.width,
|
scrollBars.vertical.width,
|
||||||
scrollBars.vertical.height
|
scrollBars.vertical.height
|
||||||
);
|
);
|
||||||
context.fillStyle = fillStyle;
|
context.fillStyle = fillStyle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportAsPNG({
|
function exportAsPNG({
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportVisibleOnly,
|
|
||||||
exportPadding = 10,
|
exportPadding = 10,
|
||||||
viewBackgroundColor
|
viewBackgroundColor
|
||||||
}: {
|
}: {
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportVisibleOnly: boolean;
|
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
}) {
|
}) {
|
||||||
if (!elements.length) return window.alert("Cannot export empty canvas.");
|
if (!elements.length) return window.alert("Cannot export empty canvas.");
|
||||||
|
|
||||||
// deselect & rerender
|
// calculate smallest area to fit the contents in
|
||||||
|
|
||||||
clearSelection();
|
let subCanvasX1 = Infinity;
|
||||||
ReactDOM.render(<App />, rootElement, () => {
|
let subCanvasX2 = 0;
|
||||||
// calculate visible-area coords
|
let subCanvasY1 = Infinity;
|
||||||
|
let subCanvasY2 = 0;
|
||||||
|
|
||||||
let subCanvasX1 = Infinity;
|
elements.forEach(element => {
|
||||||
let subCanvasX2 = 0;
|
subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element));
|
||||||
let subCanvasY1 = Infinity;
|
subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element));
|
||||||
let subCanvasY2 = 0;
|
subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element));
|
||||||
|
subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element));
|
||||||
elements.forEach(element => {
|
|
||||||
subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element));
|
|
||||||
subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element));
|
|
||||||
subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element));
|
|
||||||
subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element));
|
|
||||||
});
|
|
||||||
|
|
||||||
// create temporary canvas from which we'll export
|
|
||||||
|
|
||||||
const tempCanvas = document.createElement("canvas");
|
|
||||||
const tempCanvasCtx = tempCanvas.getContext("2d")!;
|
|
||||||
tempCanvas.style.display = "none";
|
|
||||||
document.body.appendChild(tempCanvas);
|
|
||||||
tempCanvas.width = exportVisibleOnly
|
|
||||||
? subCanvasX2 - subCanvasX1 + exportPadding * 2
|
|
||||||
: canvas.width;
|
|
||||||
tempCanvas.height = exportVisibleOnly
|
|
||||||
? subCanvasY2 - subCanvasY1 + exportPadding * 2
|
|
||||||
: canvas.height;
|
|
||||||
|
|
||||||
// if we're exporting without bg, we need to rerender the scene without it
|
|
||||||
// (it's reset again, below)
|
|
||||||
if (!exportBackground) {
|
|
||||||
renderScene(rc, context, {
|
|
||||||
viewBackgroundColor: null,
|
|
||||||
scrollX: 0,
|
|
||||||
scrollY: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy our original canvas onto the temp canvas
|
|
||||||
tempCanvasCtx.drawImage(
|
|
||||||
canvas, // source
|
|
||||||
exportVisibleOnly // sx
|
|
||||||
? subCanvasX1 - exportPadding
|
|
||||||
: 0,
|
|
||||||
exportVisibleOnly // sy
|
|
||||||
? subCanvasY1 - exportPadding
|
|
||||||
: 0,
|
|
||||||
exportVisibleOnly // sWidth
|
|
||||||
? subCanvasX2 - subCanvasX1 + exportPadding * 2
|
|
||||||
: canvas.width,
|
|
||||||
exportVisibleOnly // sHeight
|
|
||||||
? subCanvasY2 - subCanvasY1 + exportPadding * 2
|
|
||||||
: canvas.height,
|
|
||||||
0, // dx
|
|
||||||
0, // dy
|
|
||||||
exportVisibleOnly ? tempCanvas.width : canvas.width, // dWidth
|
|
||||||
exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
// reset transparent bg back to original
|
|
||||||
if (!exportBackground) {
|
|
||||||
renderScene(rc, context, { viewBackgroundColor, scrollX: 0, scrollY: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a temporary <a> elem which we'll use to download the image
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.setAttribute("download", "excalidraw.png");
|
|
||||||
link.setAttribute("href", tempCanvas.toDataURL("image/png"));
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
// clean up the DOM
|
|
||||||
link.remove();
|
|
||||||
if (tempCanvas !== canvas) tempCanvas.remove();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function distance(x: number, y: number) {
|
||||||
|
return Math.abs(x > y ? x - y : y - x);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempCanvas = document.createElement("canvas");
|
||||||
|
tempCanvas.style.display = "none";
|
||||||
|
document.body.appendChild(tempCanvas);
|
||||||
|
tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
|
||||||
|
tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
|
||||||
|
|
||||||
|
renderScene(
|
||||||
|
rough.canvas(tempCanvas),
|
||||||
|
tempCanvas,
|
||||||
|
{
|
||||||
|
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||||
|
scrollX: 0,
|
||||||
|
scrollY: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offsetX: -subCanvasX1 + exportPadding,
|
||||||
|
offsetY: -subCanvasY1 + exportPadding,
|
||||||
|
renderScrollbars: false,
|
||||||
|
renderSelection: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// create a temporary <a> elem which we'll use to download the image
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.setAttribute("download", "excalidraw.png");
|
||||||
|
link.setAttribute("href", tempCanvas.toDataURL("image/png"));
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// clean up the DOM
|
||||||
|
link.remove();
|
||||||
|
if (tempCanvas !== canvas) tempCanvas.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
|
function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
|
||||||
@ -723,8 +708,6 @@ type AppState = {
|
|||||||
resizingElement: ExcalidrawElement | null;
|
resizingElement: ExcalidrawElement | null;
|
||||||
elementType: string;
|
elementType: string;
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportVisibleOnly: boolean;
|
|
||||||
exportPadding: number;
|
|
||||||
currentItemStrokeColor: string;
|
currentItemStrokeColor: string;
|
||||||
currentItemBackgroundColor: string;
|
currentItemBackgroundColor: string;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
@ -742,25 +725,51 @@ const KEYS = {
|
|||||||
BACKSPACE: "Backspace"
|
BACKSPACE: "Backspace"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We inline font-awesome icons in order to save on js size rather than including the font awesome react library
|
||||||
const SHAPES = [
|
const SHAPES = [
|
||||||
{
|
{
|
||||||
icon: faMousePointer,
|
icon: (
|
||||||
|
// fa-mouse-pointer
|
||||||
|
<svg viewBox="0 0 320 512">
|
||||||
|
<path d="M302.189 329.126H196.105l55.831 135.993c3.889 9.428-.555 19.999-9.444 23.999l-49.165 21.427c-9.165 4-19.443-.571-23.332-9.714l-53.053-129.136-86.664 89.138C18.729 472.71 0 463.554 0 447.977V18.299C0 1.899 19.921-6.096 30.277 5.443l284.412 292.542c11.472 11.179 3.007 31.141-12.5 31.141z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
value: "selection"
|
value: "selection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: faSquare,
|
icon: (
|
||||||
|
// fa-square
|
||||||
|
<svg viewBox="0 0 448 512">
|
||||||
|
<path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
value: "rectangle"
|
value: "rectangle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: faCircle,
|
icon: (
|
||||||
|
// fa-circle
|
||||||
|
<svg viewBox="0 0 512 512">
|
||||||
|
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
value: "ellipse"
|
value: "ellipse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: faLongArrowAltRight,
|
icon: (
|
||||||
|
// fa-long-arrow-alt-right
|
||||||
|
<svg viewBox="0 0 448 512">
|
||||||
|
<path d="M313.941 216H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h301.941v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.569 0-33.941l-86.059-86.059c-15.119-15.119-40.971-4.411-40.971 16.971V216z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
value: "arrow"
|
value: "arrow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: faFont,
|
icon: (
|
||||||
|
// fa-font
|
||||||
|
<svg viewBox="0 0 448 512">
|
||||||
|
<path d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
value: "text"
|
value: "text"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -821,9 +830,7 @@ class App extends React.Component<{}, AppState> {
|
|||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
elementType: "selection",
|
elementType: "selection",
|
||||||
exportBackground: false,
|
exportBackground: true,
|
||||||
exportVisibleOnly: true,
|
|
||||||
exportPadding: 10,
|
|
||||||
currentItemStrokeColor: "#000000",
|
currentItemStrokeColor: "#000000",
|
||||||
currentItemBackgroundColor: "#ffffff",
|
currentItemBackgroundColor: "#ffffff",
|
||||||
viewBackgroundColor: "#ffffff",
|
viewBackgroundColor: "#ffffff",
|
||||||
@ -940,6 +947,8 @@ class App extends React.Component<{}, AppState> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private removeWheelEventListener: (() => void) | undefined;
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -998,9 +1007,7 @@ class App extends React.Component<{}, AppState> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="toolIcon">
|
<div className="toolIcon">{icon}</div>
|
||||||
<FontAwesomeIcon icon={icon} />
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -1050,12 +1057,7 @@ class App extends React.Component<{}, AppState> {
|
|||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
exportAsPNG({
|
exportAsPNG(this.state);
|
||||||
exportBackground: this.state.exportBackground,
|
|
||||||
exportVisibleOnly: this.state.exportVisibleOnly,
|
|
||||||
exportPadding: this.state.exportPadding,
|
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Export to png
|
Export to png
|
||||||
@ -1070,28 +1072,6 @@ class App extends React.Component<{}, AppState> {
|
|||||||
/>
|
/>
|
||||||
background
|
background
|
||||||
</label>
|
</label>
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.exportVisibleOnly}
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({ exportVisibleOnly: e.target.checked });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
visible area only
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
(padding:
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={this.state.exportPadding}
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({ exportPadding: Number(e.target.value) });
|
|
||||||
}}
|
|
||||||
disabled={!this.state.exportVisibleOnly}
|
|
||||||
/>
|
|
||||||
px)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{someElementIsSelected() && (
|
{someElementIsSelected() && (
|
||||||
<>
|
<>
|
||||||
@ -1110,13 +1090,18 @@ class App extends React.Component<{}, AppState> {
|
|||||||
id="canvas"
|
id="canvas"
|
||||||
width={window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT}
|
width={window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT}
|
||||||
height={window.innerHeight - CANVAS_WINDOW_OFFSET_TOP}
|
height={window.innerHeight - CANVAS_WINDOW_OFFSET_TOP}
|
||||||
onWheel={e => {
|
ref={canvas => {
|
||||||
e.preventDefault();
|
if (this.removeWheelEventListener) {
|
||||||
const { deltaX, deltaY } = e;
|
this.removeWheelEventListener();
|
||||||
this.setState(state => ({
|
this.removeWheelEventListener = undefined;
|
||||||
scrollX: state.scrollX - deltaX,
|
}
|
||||||
scrollY: state.scrollY - deltaY
|
if (canvas) {
|
||||||
}));
|
canvas.addEventListener("wheel", this.handleWheel, {
|
||||||
|
passive: false
|
||||||
|
});
|
||||||
|
this.removeWheelEventListener = () =>
|
||||||
|
canvas.removeEventListener("wheel", this.handleWheel);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
// only handle left mouse button
|
// only handle left mouse button
|
||||||
@ -1394,8 +1379,17 @@ class App extends React.Component<{}, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { deltaX, deltaY } = e;
|
||||||
|
this.setState(state => ({
|
||||||
|
scrollX: state.scrollX - deltaX,
|
||||||
|
scrollY: state.scrollY - deltaY
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
renderScene(rc, context, {
|
renderScene(rc, canvas, {
|
||||||
scrollX: this.state.scrollX,
|
scrollX: this.state.scrollX,
|
||||||
scrollY: this.state.scrollY,
|
scrollY: this.state.scrollY,
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor
|
viewBackgroundColor: this.state.viewBackgroundColor
|
||||||
|
@ -46,8 +46,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool {
|
.tool {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
display: none;
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
@ -62,6 +67,10 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:hover + .toolIcon {
|
&:hover + .toolIcon {
|
||||||
background-color: #e7e5e5;
|
background-color: #e7e5e5;
|
||||||
@ -69,6 +78,9 @@ body {
|
|||||||
&:checked + .toolIcon {
|
&:checked + .toolIcon {
|
||||||
background-color: #bdbebc;
|
background-color: #bdbebc;
|
||||||
}
|
}
|
||||||
|
&:focus + .toolIcon {
|
||||||
|
box-shadow: 0 0 0 2px steelblue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +101,11 @@ input[type="color"] {
|
|||||||
|
|
||||||
input {
|
input {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: transparent;
|
||||||
|
box-shadow: 0 0 0 2px steelblue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -97,7 +114,11 @@ button {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
outline: none;
|
outline: transparent;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2px steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #e7e5e5;
|
background-color: #e7e5e5;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user