|
|
|
|
@ -2,7 +2,7 @@ import React from "react";
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
|
|
import ReactDOM from "react-dom"; |
|
|
|
|
import rough from "roughjs/bin/wrappers/rough"; |
|
|
|
|
import { RoughCanvas } from "roughjs/bin/canvas"; |
|
|
|
|
import { SketchPicker } from "react-color"; |
|
|
|
|
import { TwitterPicker } from "react-color"; |
|
|
|
|
|
|
|
|
|
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex"; |
|
|
|
|
import { roundRect } from "./roundRect"; |
|
|
|
|
@ -980,16 +980,9 @@ function restore(
@@ -980,16 +980,9 @@ function restore(
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
enum ColorPicker { |
|
|
|
|
CANVAS_BACKGROUND, |
|
|
|
|
SHAPE_STROKE, |
|
|
|
|
SHAPE_BACKGROUND |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type AppState = { |
|
|
|
|
draggingElement: ExcalidrawElement | null; |
|
|
|
|
resizingElement: ExcalidrawElement | null; |
|
|
|
|
currentColorPicker: ColorPicker | null; |
|
|
|
|
elementType: string; |
|
|
|
|
exportBackground: boolean; |
|
|
|
|
currentItemStrokeColor: string; |
|
|
|
|
@ -1124,70 +1117,17 @@ const hasStroke = () =>
@@ -1124,70 +1117,17 @@ const hasStroke = () =>
|
|
|
|
|
element.type === "arrow") |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
function getSelectedFillStyles() { |
|
|
|
|
const fillStyles = Array.from( |
|
|
|
|
new Set( |
|
|
|
|
elements |
|
|
|
|
.filter(element => element.isSelected) |
|
|
|
|
.map(element => element.fillStyle) |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
return fillStyles.length === 1 ? fillStyles[0] : null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getSelectedStrokeWidth() { |
|
|
|
|
const strokeWidth = Array.from( |
|
|
|
|
new Set( |
|
|
|
|
elements |
|
|
|
|
.filter(element => element.isSelected) |
|
|
|
|
.map(element => element.strokeWidth) |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
return strokeWidth.length === 1 ? strokeWidth[0] : null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getSelectedRoughness() { |
|
|
|
|
const roughness = Array.from( |
|
|
|
|
new Set( |
|
|
|
|
elements |
|
|
|
|
.filter(element => element.isSelected) |
|
|
|
|
.map(element => element.roughness) |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
return roughness.length === 1 ? roughness[0] : null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getSelectedOpacity() { |
|
|
|
|
const opacity = Array.from( |
|
|
|
|
new Set( |
|
|
|
|
elements |
|
|
|
|
.filter(element => element.isSelected) |
|
|
|
|
.map(element => element.opacity) |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
return opacity.length === 1 ? opacity[0] : null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getSelectedStrokeColor() { |
|
|
|
|
const strokeColors = Array.from( |
|
|
|
|
new Set( |
|
|
|
|
elements |
|
|
|
|
.filter(element => element.isSelected) |
|
|
|
|
.map(element => element.strokeColor) |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
return strokeColors.length === 1 ? strokeColors[0] : null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getSelectedBackgroundColor() { |
|
|
|
|
const backgroundColors = Array.from( |
|
|
|
|
function getSelectedAttribute<T>( |
|
|
|
|
getAttribute: (element: ExcalidrawElement) => T |
|
|
|
|
): T | null { |
|
|
|
|
const attributes = Array.from( |
|
|
|
|
new Set( |
|
|
|
|
elements |
|
|
|
|
.filter(element => element.isSelected) |
|
|
|
|
.map(element => element.backgroundColor) |
|
|
|
|
.map(element => getAttribute(element)) |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
return backgroundColors.length === 1 ? backgroundColors[0] : null; |
|
|
|
|
return attributes.length === 1 ? attributes[0] : null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function addTextElement(element: ExcalidrawTextElement) { |
|
|
|
|
@ -1254,6 +1194,56 @@ function ButtonSelect<T>({
@@ -1254,6 +1194,56 @@ function ButtonSelect<T>({
|
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function ColorPicker({ |
|
|
|
|
color, |
|
|
|
|
onChange |
|
|
|
|
}: { |
|
|
|
|
color: string | null; |
|
|
|
|
onChange: (color: string) => void; |
|
|
|
|
}) { |
|
|
|
|
const [isActive, setActive] = React.useState(false); |
|
|
|
|
return ( |
|
|
|
|
<div> |
|
|
|
|
<button |
|
|
|
|
className="swatch" |
|
|
|
|
style={color ? { backgroundColor: color } : undefined} |
|
|
|
|
onClick={() => setActive(!isActive)} |
|
|
|
|
/> |
|
|
|
|
{isActive ? ( |
|
|
|
|
<div className="popover"> |
|
|
|
|
<div className="cover" onClick={() => setActive(false)} /> |
|
|
|
|
<TwitterPicker |
|
|
|
|
colors={[ |
|
|
|
|
"#000000", |
|
|
|
|
"#ABB8C3", |
|
|
|
|
"#FFFFFF", |
|
|
|
|
"#FF6900", |
|
|
|
|
"#FCB900", |
|
|
|
|
"#00D084", |
|
|
|
|
"#8ED1FC", |
|
|
|
|
"#0693E3", |
|
|
|
|
"#EB144C", |
|
|
|
|
"#F78DA7", |
|
|
|
|
"#9900EF" |
|
|
|
|
]} |
|
|
|
|
width="205px" |
|
|
|
|
color={color || undefined} |
|
|
|
|
onChange={changedColor => { |
|
|
|
|
onChange(changedColor.hex); |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
) : null} |
|
|
|
|
<input |
|
|
|
|
type="text" |
|
|
|
|
className="swatch-input" |
|
|
|
|
value={color || ""} |
|
|
|
|
onChange={e => onChange(e.target.value)} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5; |
|
|
|
|
const ELEMENT_TRANSLATE_AMOUNT = 1; |
|
|
|
|
|
|
|
|
|
@ -1282,7 +1272,6 @@ class App extends React.Component<{}, AppState> {
@@ -1282,7 +1272,6 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
draggingElement: null, |
|
|
|
|
resizingElement: null, |
|
|
|
|
elementType: "selection", |
|
|
|
|
currentColorPicker: null, |
|
|
|
|
exportBackground: true, |
|
|
|
|
currentItemStrokeColor: "#000000", |
|
|
|
|
currentItemBackgroundColor: "#ffffff", |
|
|
|
|
@ -1524,101 +1513,20 @@ class App extends React.Component<{}, AppState> {
@@ -1524,101 +1513,20 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
<button onClick={this.moveAllLeft}>Send to back</button> |
|
|
|
|
</div> |
|
|
|
|
<h5>Stroke Color</h5> |
|
|
|
|
<div> |
|
|
|
|
<button |
|
|
|
|
className="swatch" |
|
|
|
|
style={{ |
|
|
|
|
backgroundColor: |
|
|
|
|
getSelectedStrokeColor() || |
|
|
|
|
this.state.currentItemStrokeColor |
|
|
|
|
}} |
|
|
|
|
onClick={() => |
|
|
|
|
this.setState(s => ({ |
|
|
|
|
currentColorPicker: |
|
|
|
|
s.currentColorPicker === ColorPicker.SHAPE_STROKE |
|
|
|
|
? null |
|
|
|
|
: ColorPicker.SHAPE_STROKE |
|
|
|
|
})) |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
{this.state.currentColorPicker === ColorPicker.SHAPE_STROKE && ( |
|
|
|
|
<div className="popover"> |
|
|
|
|
<div |
|
|
|
|
className="cover" |
|
|
|
|
onClick={() => |
|
|
|
|
this.setState({ currentColorPicker: null }) |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
<SketchPicker |
|
|
|
|
color={this.state.currentItemStrokeColor} |
|
|
|
|
onChange={color => this.changeStrokeColor(color.hex)} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
<input |
|
|
|
|
type="text" |
|
|
|
|
className="swatch-input" |
|
|
|
|
value={ |
|
|
|
|
getSelectedStrokeColor() || |
|
|
|
|
this.state.currentItemStrokeColor |
|
|
|
|
} |
|
|
|
|
onChange={e => this.changeStrokeColor(e.target.value)} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
<ColorPicker |
|
|
|
|
color={getSelectedAttribute(element => element.strokeColor)} |
|
|
|
|
onChange={color => this.changeStrokeColor(color)} |
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
{hasBackground() && ( |
|
|
|
|
<> |
|
|
|
|
<h5>Background Color</h5> |
|
|
|
|
<div> |
|
|
|
|
<button |
|
|
|
|
className="swatch" |
|
|
|
|
style={{ |
|
|
|
|
backgroundColor: |
|
|
|
|
getSelectedBackgroundColor() || |
|
|
|
|
this.state.currentItemBackgroundColor |
|
|
|
|
}} |
|
|
|
|
onClick={() => |
|
|
|
|
this.setState(s => ({ |
|
|
|
|
currentColorPicker: |
|
|
|
|
s.currentColorPicker === |
|
|
|
|
ColorPicker.SHAPE_BACKGROUND |
|
|
|
|
? null |
|
|
|
|
: ColorPicker.SHAPE_BACKGROUND |
|
|
|
|
})) |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
{this.state.currentColorPicker === |
|
|
|
|
ColorPicker.SHAPE_BACKGROUND ? ( |
|
|
|
|
<div className="popover"> |
|
|
|
|
<div |
|
|
|
|
className="cover" |
|
|
|
|
onClick={() => |
|
|
|
|
this.setState({ currentColorPicker: null }) |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
<SketchPicker |
|
|
|
|
color={this.state.currentItemBackgroundColor} |
|
|
|
|
onChange={color => |
|
|
|
|
this.changeBackgroundColor(color.hex) |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
) : null} |
|
|
|
|
<input |
|
|
|
|
type="text" |
|
|
|
|
className="swatch-input" |
|
|
|
|
value={ |
|
|
|
|
getSelectedBackgroundColor() || |
|
|
|
|
this.state.currentItemBackgroundColor |
|
|
|
|
} |
|
|
|
|
onChange={e => this.changeBackgroundColor(e.target.value)} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
</> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
{hasBackground() && ( |
|
|
|
|
<> |
|
|
|
|
<ColorPicker |
|
|
|
|
color={getSelectedAttribute( |
|
|
|
|
element => element.backgroundColor |
|
|
|
|
)} |
|
|
|
|
onChange={color => this.changeBackgroundColor(color)} |
|
|
|
|
/> |
|
|
|
|
<h5>Fill</h5> |
|
|
|
|
<ButtonSelect |
|
|
|
|
options={[ |
|
|
|
|
@ -1626,7 +1534,7 @@ class App extends React.Component<{}, AppState> {
@@ -1626,7 +1534,7 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
{ value: "hachure", text: "Hachure" }, |
|
|
|
|
{ value: "cross-hatch", text: "Cross-hatch" } |
|
|
|
|
]} |
|
|
|
|
value={getSelectedFillStyles()} |
|
|
|
|
value={getSelectedAttribute(element => element.fillStyle)} |
|
|
|
|
onChange={value => { |
|
|
|
|
this.changeProperty(element => { |
|
|
|
|
element.fillStyle = value; |
|
|
|
|
@ -1645,7 +1553,7 @@ class App extends React.Component<{}, AppState> {
@@ -1645,7 +1553,7 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
{ value: 2, text: "Bold" }, |
|
|
|
|
{ value: 4, text: "Extra Bold" } |
|
|
|
|
]} |
|
|
|
|
value={getSelectedStrokeWidth()} |
|
|
|
|
value={getSelectedAttribute(element => element.strokeWidth)} |
|
|
|
|
onChange={value => { |
|
|
|
|
this.changeProperty(element => { |
|
|
|
|
element.strokeWidth = value; |
|
|
|
|
@ -1653,14 +1561,14 @@ class App extends React.Component<{}, AppState> {
@@ -1653,14 +1561,14 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
<h5>Slopiness</h5> |
|
|
|
|
<h5>Sloppiness</h5> |
|
|
|
|
<ButtonSelect |
|
|
|
|
options={[ |
|
|
|
|
{ value: 0, text: "Draftsman" }, |
|
|
|
|
{ value: 1, text: "Artist" }, |
|
|
|
|
{ value: 3, text: "Cartoonist" } |
|
|
|
|
]} |
|
|
|
|
value={getSelectedRoughness()} |
|
|
|
|
value={getSelectedAttribute(element => element.roughness)} |
|
|
|
|
onChange={value => |
|
|
|
|
this.changeProperty(element => { |
|
|
|
|
element.roughness = value; |
|
|
|
|
@ -1677,7 +1585,7 @@ class App extends React.Component<{}, AppState> {
@@ -1677,7 +1585,7 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
max="100" |
|
|
|
|
onChange={this.changeOpacity} |
|
|
|
|
value={ |
|
|
|
|
getSelectedOpacity() || |
|
|
|
|
getSelectedAttribute(element => element.opacity) || |
|
|
|
|
0 /* Put the opacity at 0 if there are two conflicting ones */ |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
@ -1690,45 +1598,10 @@ class App extends React.Component<{}, AppState> {
@@ -1690,45 +1598,10 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
<h4>Canvas</h4> |
|
|
|
|
<div className="panelColumn"> |
|
|
|
|
<h5>Canvas Background Color</h5> |
|
|
|
|
<div> |
|
|
|
|
<button |
|
|
|
|
className="swatch" |
|
|
|
|
style={{ |
|
|
|
|
backgroundColor: this.state.viewBackgroundColor |
|
|
|
|
}} |
|
|
|
|
onClick={() => |
|
|
|
|
this.setState(s => ({ |
|
|
|
|
currentColorPicker: |
|
|
|
|
s.currentColorPicker === ColorPicker.CANVAS_BACKGROUND |
|
|
|
|
? null |
|
|
|
|
: ColorPicker.CANVAS_BACKGROUND |
|
|
|
|
})) |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
{this.state.currentColorPicker === |
|
|
|
|
ColorPicker.CANVAS_BACKGROUND ? ( |
|
|
|
|
<div className="popover"> |
|
|
|
|
<div |
|
|
|
|
className="cover" |
|
|
|
|
onClick={() => this.setState({ currentColorPicker: null })} |
|
|
|
|
/> |
|
|
|
|
<SketchPicker |
|
|
|
|
color={this.state.viewBackgroundColor} |
|
|
|
|
onChange={color => { |
|
|
|
|
this.setState({ viewBackgroundColor: color.hex }); |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
) : null} |
|
|
|
|
<input |
|
|
|
|
type="text" |
|
|
|
|
className="swatch-input" |
|
|
|
|
value={this.state.viewBackgroundColor} |
|
|
|
|
onChange={e => |
|
|
|
|
this.setState({ viewBackgroundColor: e.target.value }) |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
<ColorPicker |
|
|
|
|
color={this.state.viewBackgroundColor} |
|
|
|
|
onChange={color => this.setState({ viewBackgroundColor: color })} |
|
|
|
|
/> |
|
|
|
|
<button |
|
|
|
|
onClick={this.clearCanvas} |
|
|
|
|
title="Clear the canvas & reset background color" |
|
|
|
|
|