implement stroke style (#1571)

This commit is contained in:
David Luzar 2020-05-14 17:04:33 +02:00 committed by GitHub
parent f6be200388
commit 39c56a4c01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 459 additions and 9 deletions

View File

@ -227,6 +227,41 @@ export const actionChangeSloppiness = register({
),
});
export const actionChangeStrokeStyle = register({
name: "changeStrokeStyle",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
strokeStyle: value,
}),
),
appState: { ...appState, currentItemStrokeStyle: value },
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.strokeStyle")}</legend>
<ButtonSelect
group="strokeStyle"
options={[
{ value: "solid", text: t("labels.strokeStyle_solid") },
{ value: "dashed", text: t("labels.strokeStyle_dashed") },
{ value: "dotted", text: t("labels.strokeStyle_dotted") },
]}
value={getFormValue(
elements,
appState,
(element) => element.strokeStyle,
appState.currentItemStrokeStyle,
)}
onChange={(value) => updateData(value)}
/>
</fieldset>
),
});
export const actionChangeOpacity = register({
name: "changeOpacity",
perform: (elements, appState, value) => {

View File

@ -30,6 +30,7 @@ export type ActionName =
| "changeFillStyle"
| "changeStrokeWidth"
| "changeSloppiness"
| "changeStrokeStyle"
| "changeOpacity"
| "changeFontSize"
| "toggleCanvasMenu"

View File

@ -22,6 +22,7 @@ export function getDefaultAppState(): AppState {
currentItemBackgroundColor: "transparent",
currentItemFillStyle: "hachure",
currentItemStrokeWidth: 1,
currentItemStrokeStyle: "solid",
currentItemRoughness: 1,
currentItemOpacity: 100,
currentItemFont: DEFAULT_FONT,

View File

@ -45,7 +45,7 @@ export function SelectedShapeActions({
targetElements.some((element) => hasStroke(element.type))) && (
<>
{renderAction("changeStrokeWidth")}
{renderAction("changeStrokeStyle")}
{renderAction("changeSloppiness")}
</>
)}

View File

@ -729,6 +729,7 @@ class App extends React.Component<any, AppState> {
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
text: text,
@ -1357,6 +1358,7 @@ class App extends React.Component<any, AppState> {
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
text: "",
@ -2037,6 +2039,7 @@ class App extends React.Component<any, AppState> {
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
});
@ -2067,6 +2070,7 @@ class App extends React.Component<any, AppState> {
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
});

View File

@ -76,6 +76,7 @@ export function restore(
id: element.id || randomId(),
fillStyle: element.fillStyle || "hachure",
strokeWidth: element.strokeWidth || 1,
strokeStyle: element.strokeStyle ?? "solid",
roughness: element.roughness ?? 1,
opacity:
element.opacity === null || element.opacity === undefined

View File

@ -30,6 +30,7 @@ it("clones arrow element", () => {
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
});
@ -73,6 +74,7 @@ it("clones text element", () => {
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
text: "hello",

View File

@ -17,6 +17,7 @@ type ElementConstructorOpts = {
backgroundColor: ExcalidrawGenericElement["backgroundColor"];
fillStyle: ExcalidrawGenericElement["fillStyle"];
strokeWidth: ExcalidrawGenericElement["strokeWidth"];
strokeStyle: ExcalidrawGenericElement["strokeStyle"];
roughness: ExcalidrawGenericElement["roughness"];
opacity: ExcalidrawGenericElement["opacity"];
width?: ExcalidrawGenericElement["width"];
@ -33,6 +34,7 @@ function _newElementBase<T extends ExcalidrawElement>(
backgroundColor,
fillStyle,
strokeWidth,
strokeStyle,
roughness,
opacity,
width = 0,
@ -53,6 +55,7 @@ function _newElementBase<T extends ExcalidrawElement>(
backgroundColor,
fillStyle,
strokeWidth,
strokeStyle,
roughness,
opacity,
seed: rest.seed ?? randomInteger(),

View File

@ -8,6 +8,7 @@ type _ExcalidrawElementBase = Readonly<{
backgroundColor: string;
fillStyle: string;
strokeWidth: number;
strokeStyle: "solid" | "dashed" | "dotted";
roughness: number;
opacity: number;
width: number;

View File

@ -16,6 +16,10 @@
"background": "Background",
"fill": "Fill",
"strokeWidth": "Stroke width",
"strokeStyle": "Stroke style",
"strokeStyle_solid": "Solid",
"strokeStyle_dashed": "Dashed",
"strokeStyle_dotted": "Dotted",
"sloppiness": "Sloppiness",
"opacity": "Opacity",
"textAlign": "Text align",

View File

@ -20,6 +20,9 @@ import rough from "roughjs/bin/rough";
const CANVAS_PADDING = 20;
const DASHARRAY_DASHED = [12, 8];
const DASHARRAY_DOTTED = [3, 6];
export interface ExcalidrawElementWithCanvas {
element: ExcalidrawElement | ExcalidrawTextElement;
canvas: HTMLCanvasElement;
@ -90,9 +93,9 @@ function drawElementOnCanvas(
case "arrow":
case "draw":
case "line": {
(getShapeForElement(element) as Drawable[]).forEach((shape) =>
rc.draw(shape),
);
(getShapeForElement(element) as Drawable[]).forEach((shape) => {
rc.draw(shape);
});
break;
}
default: {
@ -157,16 +160,42 @@ function generateElement(
let shape = shapeCache.get(element) || null;
if (!shape) {
elementWithCanvasCache.delete(element);
const strokeLineDash =
element.strokeStyle === "dashed"
? DASHARRAY_DASHED
: element.strokeStyle === "dotted"
? DASHARRAY_DOTTED
: undefined;
// for non-solid strokes, disable multiStroke because it tends to make
// dashes/dots overlay each other
const disableMultiStroke = element.strokeStyle !== "solid";
// for non-solid strokes, increase the width a bit to make it visually
// similar to solid strokes, because we're also disabling multiStroke
const strokeWidth =
element.strokeStyle !== "solid"
? element.strokeWidth + 0.5
: element.strokeWidth;
// when increasing strokeWidth, we must explicitly set fillWeight and
// hachureGap because if not specified, roughjs uses strokeWidth to
// calculate them (and we don't want the fills to be modified)
const fillWeight = element.strokeWidth / 2;
const hachureGap = element.strokeWidth * 4;
switch (element.type) {
case "rectangle":
shape = generator.rectangle(0, 0, element.width, element.height, {
strokeWidth,
fillWeight,
hachureGap,
strokeLineDash,
disableMultiStroke,
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
});
@ -191,13 +220,17 @@ function generateElement(
[leftX, leftY],
],
{
strokeWidth,
fillWeight,
hachureGap,
strokeLineDash,
disableMultiStroke,
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
},
@ -211,13 +244,17 @@ function generateElement(
element.width,
element.height,
{
strokeWidth,
fillWeight,
hachureGap,
strokeLineDash,
disableMultiStroke,
stroke: element.strokeColor,
fill:
element.backgroundColor === "transparent"
? undefined
: element.backgroundColor,
fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
curveFitting: 1,
@ -228,10 +265,14 @@ function generateElement(
case "draw":
case "arrow": {
const options: Options = {
strokeWidth,
fillWeight,
hachureGap,
strokeLineDash,
disableMultiStroke,
stroke: element.strokeColor,
strokeWidth: element.strokeWidth,
roughness: element.roughness,
seed: element.seed,
roughness: element.roughness,
};
// points array can be empty in the beginning, so it is important to add
@ -257,6 +298,13 @@ function generateElement(
// add lines only in arrow
if (element.type === "arrow") {
const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element, shape);
// for dotted arrows caps, reduce gap to make it more legible
if (element.strokeStyle === "dotted") {
options.strokeLineDash = [3, 4];
// for solid/dashed, keep solid arrow cap
} else {
delete options.strokeLineDash;
}
shape.push(
...[
generator.line(x3, y3, x2, y2, options),

View File

@ -165,6 +165,7 @@ function getWatermarkElement(maxX: number, maxY: number) {
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
});

View File

@ -25,6 +25,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "arrow",
"version": 3,
@ -49,6 +50,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "diamond",
"version": 2,
@ -73,6 +75,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "ellipse",
"version": 2,
@ -106,6 +109,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "line",
"version": 3,
@ -130,6 +134,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,

View File

@ -12,6 +12,7 @@ Object {
"roughness": 1,
"seed": 2019559783,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
@ -34,6 +35,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 5,
@ -56,6 +58,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,

View File

@ -30,6 +30,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "arrow",
"version": 7,
@ -70,6 +71,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "line",
"version": 7,

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,
@ -34,6 +35,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 3,

View File

@ -23,6 +23,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "arrow",
"version": 3,
@ -56,6 +57,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "line",
"version": 3,
@ -78,6 +80,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "diamond",
"version": 2,
@ -100,6 +103,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "ellipse",
"version": 2,
@ -122,6 +126,7 @@ Object {
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,

View File

@ -36,6 +36,7 @@ function populateElements(
backgroundColor: h.state.currentItemBackgroundColor,
fillStyle: h.state.currentItemFillStyle,
strokeWidth: h.state.currentItemStrokeWidth,
strokeStyle: h.state.currentItemStrokeStyle,
roughness: h.state.currentItemRoughness,
opacity: h.state.currentItemOpacity,
});

View File

@ -4,6 +4,7 @@ import {
NonDeletedExcalidrawElement,
NonDeleted,
TextAlign,
ExcalidrawElement,
} from "./element/types";
import { SHAPES } from "./shapes";
import { Point as RoughPoint } from "roughjs/bin/geometry";
@ -30,6 +31,7 @@ export type AppState = {
currentItemBackgroundColor: string;
currentItemFillStyle: string;
currentItemStrokeWidth: number;
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
currentItemRoughness: number;
currentItemOpacity: number;
currentItemFont: string;