tweak stroke widths

This commit is contained in:
Ryan Di 2025-06-16 22:06:10 +10:00
parent c72c47f0cd
commit b1f3cc50ee
10 changed files with 111 additions and 66 deletions

View File

@ -385,8 +385,9 @@ export const ROUGHNESS = {
export const STROKE_WIDTH = { export const STROKE_WIDTH = {
thin: 1, thin: 1,
bold: 2, medium: 2,
extraBold: 4, bold: 4,
extraBold: 6,
} as const; } as const;
export const DEFAULT_ELEMENT_PROPS: { export const DEFAULT_ELEMENT_PROPS: {
@ -402,7 +403,7 @@ export const DEFAULT_ELEMENT_PROPS: {
strokeColor: COLOR_PALETTE.black, strokeColor: COLOR_PALETTE.black,
backgroundColor: COLOR_PALETTE.transparent, backgroundColor: COLOR_PALETTE.transparent,
fillStyle: "solid", fillStyle: "solid",
strokeWidth: 2, strokeWidth: STROKE_WIDTH.medium,
strokeStyle: "solid", strokeStyle: "solid",
roughness: ROUGHNESS.artist, roughness: ROUGHNESS.artist,
opacity: 100, opacity: 100,

View File

@ -10,7 +10,7 @@ import type { ExcalidrawFreeDrawElement } from "./types";
export const DRAWING_CONFIGS = { export const DRAWING_CONFIGS = {
default: { default: {
streamline: 0.25, streamline: 0.35,
simplify: 0.25, simplify: 0.25,
}, },
// for optimal performance, we use a lower streamline and simplify // for optimal performance, we use a lower streamline and simplify
@ -62,10 +62,7 @@ const calculateVelocityBasedPressure = (
return Math.max(0.1, Math.min(1.0, pressure)); return Math.max(0.1, Math.min(1.0, pressure));
}; };
export const getFreedrawStroke = ( export const getFreedrawStroke = (element: ExcalidrawFreeDrawElement) => {
element: ExcalidrawFreeDrawElement,
debugParams?: { streamline?: number; simplify?: number },
) => {
// Compose points as [x, y, pressure] // Compose points as [x, y, pressure]
let points: [number, number, number][]; let points: [number, number, number][];
if (element.simulatePressure) { if (element.simulatePressure) {
@ -105,17 +102,15 @@ export const getFreedrawStroke = (
streamline, streamline,
simplify, simplify,
sizeMapping: ({ pressure: t }) => { sizeMapping: ({ pressure: t }) => {
if (element.simulatePressure) {
return t + 0.2;
}
if (element.drawingConfigs?.pressureSensitivity === 0) { if (element.drawingConfigs?.pressureSensitivity === 0) {
return 1; return 0.5;
} }
const minSize = 0.2; if (element.simulatePressure) {
const maxSize = 2; return 0.2 + t * 0.6;
return minSize + t * (maxSize - minSize); }
return 0.2 + t * 0.8;
}, },
}); });
@ -134,14 +129,13 @@ export const getFreedrawStroke = (
*/ */
export const getFreeDrawSvgPath = ( export const getFreeDrawSvgPath = (
element: ExcalidrawFreeDrawElement, element: ExcalidrawFreeDrawElement,
debugParams?: { streamline?: number; simplify?: number },
): string => { ): string => {
// legacy, for backwards compatibility // legacy, for backwards compatibility
if (element.drawingConfigs === null) { if (element.drawingConfigs === null) {
return _legacy_getFreeDrawSvgPath(element); return _legacy_getFreeDrawSvgPath(element);
} }
return getSvgPathFromStroke(getFreedrawStroke(element, debugParams)); return getSvgPathFromStroke(getFreedrawStroke(element));
}; };
const roundPoint = (A: Point): string => { const roundPoint = (A: Point): string => {

View File

@ -145,26 +145,27 @@ describe("element locking", () => {
queryByTestId(document.body, `strokeWidth-thin`), queryByTestId(document.body, `strokeWidth-thin`),
).not.toBeChecked(); ).not.toBeChecked();
expect( expect(
queryByTestId(document.body, `strokeWidth-bold`), queryByTestId(document.body, `strokeWidth-medium`),
).not.toBeChecked(); ).not.toBeChecked();
expect( expect(
queryByTestId(document.body, `strokeWidth-extraBold`), queryByTestId(document.body, `strokeWidth-bold`),
).not.toBeChecked(); ).not.toBeChecked();
}); });
it("should show properties of different element types when selected", () => { it("should show properties of different element types when selected", () => {
const rect = API.createElement({ const rect = API.createElement({
type: "rectangle", type: "rectangle",
strokeWidth: STROKE_WIDTH.bold, strokeWidth: STROKE_WIDTH.medium,
}); });
const text = API.createElement({ const text = API.createElement({
type: "text", type: "text",
fontFamily: FONT_FAMILY["Comic Shanns"], fontFamily: FONT_FAMILY["Comic Shanns"],
strokeWidth: undefined,
}); });
API.setElements([rect, text]); API.setElements([rect, text]);
API.setSelectedElements([rect, text]); API.setSelectedElements([rect, text]);
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked(); expect(queryByTestId(document.body, `strokeWidth-medium`)).toBeChecked();
expect(queryByTestId(document.body, `font-family-code`)).toHaveClass( expect(queryByTestId(document.body, `font-family-code`)).toHaveClass(
"active", "active",
); );

View File

@ -130,6 +130,7 @@ import {
ArrowheadCrowfootOneOrManyIcon, ArrowheadCrowfootOneOrManyIcon,
strokeWidthFixedIcon, strokeWidthFixedIcon,
strokeWidthVariableIcon, strokeWidthVariableIcon,
StrokeWidthMediumIcon,
} from "../components/icons"; } from "../components/icons";
import { Fonts } from "../fonts"; import { Fonts } from "../fonts";
@ -509,6 +510,33 @@ export const actionChangeFillStyle = register({
}, },
}); });
const WIDTHS = [
{
value: STROKE_WIDTH.thin,
text: t("labels.thin"),
icon: StrokeWidthBaseIcon,
testId: "strokeWidth-thin",
},
{
value: STROKE_WIDTH.medium,
text: t("labels.medium"),
icon: StrokeWidthMediumIcon,
testId: "strokeWidth-medium",
},
{
value: STROKE_WIDTH.bold,
text: t("labels.bold"),
icon: StrokeWidthBoldIcon,
testId: "strokeWidth-bold",
},
{
value: STROKE_WIDTH.extraBold,
text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-extraBold",
},
];
export const actionChangeStrokeWidth = register({ export const actionChangeStrokeWidth = register({
name: "changeStrokeWidth", name: "changeStrokeWidth",
label: "labels.strokeWidth", label: "labels.strokeWidth",
@ -530,26 +558,11 @@ export const actionChangeStrokeWidth = register({
<div className="buttonList"> <div className="buttonList">
<RadioSelection <RadioSelection
group="stroke-width" group="stroke-width"
options={[ options={
{ appState.activeTool.type === "freedraw"
value: STROKE_WIDTH.thin, ? WIDTHS
text: t("labels.thin"), : WIDTHS.slice(0, 3)
icon: StrokeWidthBaseIcon, }
testId: "strokeWidth-thin",
},
{
value: STROKE_WIDTH.bold,
text: t("labels.bold"),
icon: StrokeWidthBoldIcon,
testId: "strokeWidth-bold",
},
{
value: STROKE_WIDTH.extraBold,
text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-extraBold",
},
]}
value={getFormValue( value={getFormValue(
elements, elements,
app, app,

View File

@ -1136,7 +1136,7 @@ export const StrokeWidthBaseIcon = createIcon(
modifiedTablerIconProps, modifiedTablerIconProps,
); );
export const StrokeWidthBoldIcon = createIcon( export const StrokeWidthMediumIcon = createIcon(
<path <path
d="M5 10h10" d="M5 10h10"
stroke="currentColor" stroke="currentColor"
@ -1147,7 +1147,7 @@ export const StrokeWidthBoldIcon = createIcon(
modifiedTablerIconProps, modifiedTablerIconProps,
); );
export const StrokeWidthExtraBoldIcon = createIcon( export const StrokeWidthBoldIcon = createIcon(
<path <path
d="M5 10h10" d="M5 10h10"
stroke="currentColor" stroke="currentColor"
@ -1158,6 +1158,17 @@ export const StrokeWidthExtraBoldIcon = createIcon(
modifiedTablerIconProps, modifiedTablerIconProps,
); );
export const StrokeWidthExtraBoldIcon = createIcon(
<path
d="M5 10h10"
stroke="currentColor"
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
/>,
modifiedTablerIconProps,
);
export const StrokeStyleSolidIcon = React.memo(({ theme }: { theme: Theme }) => export const StrokeStyleSolidIcon = React.memo(({ theme }: { theme: Theme }) =>
createIcon( createIcon(
<path <path

View File

@ -3127,7 +3127,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"currentItemStartArrowhead": null, "currentItemStartArrowhead": null,
"currentItemStrokeColor": "#e03131", "currentItemStrokeColor": "#e03131",
"currentItemStrokeStyle": "dotted", "currentItemStrokeStyle": "dotted",
"currentItemStrokeWidth": 2, "currentItemStrokeWidth": 4,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
@ -3241,11 +3241,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"seed": 449462985, "seed": 449462985,
"strokeColor": "#e03131", "strokeColor": "#e03131",
"strokeStyle": "dotted", "strokeStyle": "dotted",
"strokeWidth": 2, "strokeWidth": 4,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 941653321, "versionNonce": 1402203177,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -3272,14 +3272,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roundness": { "roundness": {
"type": 3, "type": 3,
}, },
"seed": 289600103, "seed": 1898319239,
"strokeColor": "#e03131", "strokeColor": "#e03131",
"strokeStyle": "dotted", "strokeStyle": "dotted",
"strokeWidth": 2, "strokeWidth": 4,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 9, "version": 10,
"versionNonce": 640725609, "versionNonce": 941653321,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@ -3288,7 +3288,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`; exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`;
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `16`; exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `17`;
exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] redo stack 1`] = `[]`; exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] redo stack 1`] = `[]`;
@ -3469,6 +3469,29 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
}, },
"id": "id11", "id": "id11",
}, },
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id3": {
"deleted": {
"strokeWidth": 4,
},
"inserted": {
"strokeWidth": 2,
},
},
},
},
"id": "id13",
},
{ {
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
@ -3490,7 +3513,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
}, },
}, },
}, },
"id": "id13", "id": "id15",
}, },
{ {
"appState": AppStateDelta { "appState": AppStateDelta {
@ -3513,7 +3536,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
}, },
}, },
}, },
"id": "id15", "id": "id17",
}, },
{ {
"appState": AppStateDelta { "appState": AppStateDelta {
@ -3536,7 +3559,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
}, },
}, },
}, },
"id": "id17", "id": "id19",
}, },
{ {
"appState": AppStateDelta { "appState": AppStateDelta {
@ -3565,6 +3588,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 2, "roughness": 2,
"strokeColor": "#e03131", "strokeColor": "#e03131",
"strokeStyle": "dotted", "strokeStyle": "dotted",
"strokeWidth": 4,
}, },
"inserted": { "inserted": {
"backgroundColor": "transparent", "backgroundColor": "transparent",
@ -3573,11 +3597,12 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 1, "roughness": 1,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2,
}, },
}, },
}, },
}, },
"id": "id19", "id": "id21",
}, },
] ]
`; `;

View File

@ -8913,7 +8913,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -9022,7 +9022,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -12075,7 +12075,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -12130,7 +12130,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -12275,7 +12275,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,

View File

@ -6880,7 +6880,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -9159,7 +9159,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -10167,7 +10167,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
"drawingConfigs": { "drawingConfigs": {
"pressureSensitivity": 1, "pressureSensitivity": 1,
"simplify": "0.25000", "simplify": "0.25000",
"streamline": "0.25000", "streamline": "0.35000",
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,

View File

@ -78,7 +78,7 @@ describe("actionStyles", () => {
expect(firstRect.strokeColor).toBe("#e03131"); expect(firstRect.strokeColor).toBe("#e03131");
expect(firstRect.backgroundColor).toBe("#a5d8ff"); expect(firstRect.backgroundColor).toBe("#a5d8ff");
expect(firstRect.fillStyle).toBe("cross-hatch"); expect(firstRect.fillStyle).toBe("cross-hatch");
expect(firstRect.strokeWidth).toBe(2); // Bold: 2 expect(firstRect.strokeWidth).toBe(4); // Bold: 4
expect(firstRect.strokeStyle).toBe("dotted"); expect(firstRect.strokeStyle).toBe("dotted");
expect(firstRect.roughness).toBe(2); // Cartoonist: 2 expect(firstRect.roughness).toBe(2); // Cartoonist: 2
expect(firstRect.opacity).toBe(60); expect(firstRect.opacity).toBe(60);

View File

@ -381,7 +381,7 @@ describe("contextMenu element", () => {
expect(firstRect.strokeColor).toBe("#e03131"); expect(firstRect.strokeColor).toBe("#e03131");
expect(firstRect.backgroundColor).toBe("#a5d8ff"); expect(firstRect.backgroundColor).toBe("#a5d8ff");
expect(firstRect.fillStyle).toBe("cross-hatch"); expect(firstRect.fillStyle).toBe("cross-hatch");
expect(firstRect.strokeWidth).toBe(2); // Bold: 2 expect(firstRect.strokeWidth).toBe(4); // Bold: 4
expect(firstRect.strokeStyle).toBe("dotted"); expect(firstRect.strokeStyle).toBe("dotted");
expect(firstRect.roughness).toBe(2); // Cartoonist: 2 expect(firstRect.roughness).toBe(2); // Cartoonist: 2
expect(firstRect.opacity).toBe(60); expect(firstRect.opacity).toBe(60);