{isShade && "⇧"}
diff --git a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
index 50594a59e1..38e5cf8c5e 100644
--- a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
+++ b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
@@ -65,7 +65,7 @@ const PickerColorList = ({
tabIndex={-1}
type="button"
className={clsx(
- "color-picker__button color-picker__button--large",
+ "color-picker__button color-picker__button--large has-outline",
{
active: colorObj?.colorName === key,
"is-transparent": color === "transparent" || !color,
diff --git a/packages/excalidraw/components/ColorPicker/ShadeList.tsx b/packages/excalidraw/components/ColorPicker/ShadeList.tsx
index aa2c25ea0d..1c8e4c4eb4 100644
--- a/packages/excalidraw/components/ColorPicker/ShadeList.tsx
+++ b/packages/excalidraw/components/ColorPicker/ShadeList.tsx
@@ -55,7 +55,7 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
key={i}
type="button"
className={clsx(
- "color-picker__button color-picker__button--large",
+ "color-picker__button color-picker__button--large has-outline",
{ active: i === shade },
)}
aria-label="Shade"
diff --git a/packages/excalidraw/components/ColorPicker/TopPicks.tsx b/packages/excalidraw/components/ColorPicker/TopPicks.tsx
index 6d18a95871..8531172fb3 100644
--- a/packages/excalidraw/components/ColorPicker/TopPicks.tsx
+++ b/packages/excalidraw/components/ColorPicker/TopPicks.tsx
@@ -1,11 +1,14 @@
import clsx from "clsx";
import {
+ COLOR_OUTLINE_CONTRAST_THRESHOLD,
DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_PICKS,
} from "@excalidraw/common";
+import { isColorDark } from "./colorPickerUtils";
+
import type { ColorPickerType } from "./colorPickerUtils";
interface TopPicksProps {
@@ -51,6 +54,10 @@ export const TopPicks = ({
className={clsx("color-picker__button", {
active: color === activeColor,
"is-transparent": color === "transparent" || !color,
+ "has-outline": !isColorDark(
+ color,
+ COLOR_OUTLINE_CONTRAST_THRESHOLD,
+ ),
})}
style={{ "--swatch-color": color }}
key={color}
diff --git a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
index 4925a31451..f572bd49f3 100644
--- a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
+++ b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
@@ -93,19 +93,42 @@ export type ActiveColorPickerSectionAtomType =
export const activeColorPickerSectionAtom =
atom
(null);
-const calculateContrast = (r: number, g: number, b: number) => {
+const calculateContrast = (r: number, g: number, b: number): number => {
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
- return yiq >= 160 ? "black" : "white";
+ return yiq;
};
-// inspiration from https://stackoverflow.com/a/11868398
-export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
- if (isCustomColor) {
- const style = new Option().style;
- style.color = bgHex;
+// YIQ algo, inspiration from https://stackoverflow.com/a/11868398
+export const isColorDark = (color: string, threshold = 160): boolean => {
+ // no color ("") -> assume it default to black
+ if (!color) {
+ return true;
+ }
- if (style.color) {
- const rgb = style.color
+ if (color === "transparent") {
+ return false;
+ }
+
+ // a string color (white etc) or any other format -> convert to rgb by way
+ // of creating a DOM node and retrieving the computeStyle
+ if (!color.startsWith("#")) {
+ const node = document.createElement("div");
+ node.style.color = color;
+
+ if (node.style.color) {
+ // making invisible so document doesn't reflow (hopefully).
+ // display=none works too, but supposedly not in all browsers
+ node.style.position = "absolute";
+ node.style.visibility = "hidden";
+ node.style.width = "0";
+ node.style.height = "0";
+
+ // needs to be in DOM else browser won't compute the style
+ document.body.appendChild(node);
+ const computedColor = getComputedStyle(node).color;
+ document.body.removeChild(node);
+ // computed style is in rgb() format
+ const rgb = computedColor
.replace(/^(rgb|rgba)\(/, "")
.replace(/\)$/, "")
.replace(/\s/g, "")
@@ -114,20 +137,17 @@ export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
const g = parseInt(rgb[1]);
const b = parseInt(rgb[2]);
- return calculateContrast(r, g, b);
+ return calculateContrast(r, g, b) < threshold;
}
+ // invalid color -> assume it default to black
+ return true;
}
- // TODO: ? is this wanted?
- if (bgHex === "transparent") {
- return "black";
- }
+ const r = parseInt(color.slice(1, 3), 16);
+ const g = parseInt(color.slice(3, 5), 16);
+ const b = parseInt(color.slice(5, 7), 16);
- const r = parseInt(bgHex.substring(1, 3), 16);
- const g = parseInt(bgHex.substring(3, 5), 16);
- const b = parseInt(bgHex.substring(5, 7), 16);
-
- return calculateContrast(r, g, b);
+ return calculateContrast(r, g, b) < threshold;
};
export type ColorPickerType =
diff --git a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
index 4391759d9b..8b45e3377e 100644
--- a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
+++ b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
@@ -315,6 +315,7 @@ function CommandPaletteInner({
const toolCommands: CommandPaletteItem[] = [
actionManager.actions.toggleHandTool,
actionManager.actions.setFrameAsActiveTool,
+ actionManager.actions.toggleLassoTool,
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
const editorCommands: CommandPaletteItem[] = [
diff --git a/packages/excalidraw/components/ElementLinkDialog.tsx b/packages/excalidraw/components/ElementLinkDialog.tsx
index 5a0b9107ba..e9766f3d7b 100644
--- a/packages/excalidraw/components/ElementLinkDialog.tsx
+++ b/packages/excalidraw/components/ElementLinkDialog.tsx
@@ -6,9 +6,10 @@ import {
defaultGetElementLinkFromSelection,
getLinkIdAndTypeFromSelection,
} from "@excalidraw/element/elementLink";
-import { mutateElement } from "@excalidraw/element/mutateElement";
-import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import type Scene from "@excalidraw/element/Scene";
import { t } from "../i18n";
import { getSelectedElements } from "../scene";
@@ -21,20 +22,20 @@ import { TrashIcon } from "./icons";
import "./ElementLinkDialog.scss";
import type { AppProps, AppState, UIAppState } from "../types";
-
const ElementLinkDialog = ({
sourceElementId,
onClose,
- elementsMap,
appState,
+ scene,
generateLinkForSelection = defaultGetElementLinkFromSelection,
}: {
sourceElementId: ExcalidrawElement["id"];
- elementsMap: ElementsMap;
appState: UIAppState;
+ scene: Scene;
onClose?: () => void;
generateLinkForSelection: AppProps["generateLinkForSelection"];
}) => {
+ const elementsMap = scene.getNonDeletedElementsMap();
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
const [nextLink, setNextLink] = useState(originalLink);
@@ -70,7 +71,7 @@ const ElementLinkDialog = ({
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
const elementToLink = elementsMap.get(sourceElementId);
elementToLink &&
- mutateElement(elementToLink, {
+ scene.mutateElement(elementToLink, {
link: nextLink,
});
}
@@ -78,13 +79,13 @@ const ElementLinkDialog = ({
if (!nextLink && linkEdited && sourceElementId) {
const elementToLink = elementsMap.get(sourceElementId);
elementToLink &&
- mutateElement(elementToLink, {
+ scene.mutateElement(elementToLink, {
link: null,
});
}
onClose?.();
- }, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
+ }, [sourceElementId, nextLink, elementsMap, linkEdited, scene, onClose]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
diff --git a/packages/excalidraw/components/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx
index 6eb1a21867..5072e4471f 100644
--- a/packages/excalidraw/components/HintViewer.tsx
+++ b/packages/excalidraw/components/HintViewer.tsx
@@ -120,7 +120,7 @@ const getHints = ({
!appState.editingTextElement &&
!appState.editingLinearElement
) {
- return t("hints.deepBoxSelect");
+ return [t("hints.deepBoxSelect")];
}
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
@@ -128,7 +128,7 @@ const getHints = ({
}
if (!selectedElements.length && !isMobile) {
- return t("hints.canvasPanning");
+ return [t("hints.canvasPanning")];
}
if (selectedElements.length === 1) {
diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx
index b5491dedd7..b2e0d446fa 100644
--- a/packages/excalidraw/components/LayerUI.tsx
+++ b/packages/excalidraw/components/LayerUI.tsx
@@ -5,6 +5,7 @@ import {
CLASSES,
DEFAULT_SIDEBAR,
TOOL_TYPE,
+ arrayToMap,
capitalizeString,
isShallowEqual,
} from "@excalidraw/common";
@@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
-import Scene from "../scene/Scene";
import { actionToggleStats } from "../actions";
import { trackEvent } from "../analytics";
import { isHandToolActive } from "../appState";
@@ -446,22 +446,18 @@ const LayerUI = ({
if (selectedElements.length) {
for (const element of selectedElements) {
- mutateElement(
- element,
- {
- [altKey && eyeDropperState.swapPreviewOnAlt
- ? colorPickerType === "elementBackground"
- ? "strokeColor"
- : "backgroundColor"
- : colorPickerType === "elementBackground"
- ? "backgroundColor"
- : "strokeColor"]: color,
- },
- false,
- );
+ mutateElement(element, arrayToMap(elements), {
+ [altKey && eyeDropperState.swapPreviewOnAlt
+ ? colorPickerType === "elementBackground"
+ ? "strokeColor"
+ : "backgroundColor"
+ : colorPickerType === "elementBackground"
+ ? "backgroundColor"
+ : "strokeColor"]: color,
+ });
ShapeCache.delete(element);
}
- Scene.getScene(selectedElements[0])?.triggerUpdate();
+ app.scene.triggerUpdate();
} else if (colorPickerType === "elementBackground") {
setAppState({
currentItemBackgroundColor: color,
@@ -494,7 +490,7 @@ const LayerUI = ({
openDialog: null,
});
}}
- elementsMap={app.scene.getNonDeletedElementsMap()}
+ scene={app.scene}
appState={appState}
generateLinkForSelection={generateLinkForSelection}
/>
diff --git a/packages/excalidraw/components/LibraryMenuItems.tsx b/packages/excalidraw/components/LibraryMenuItems.tsx
index f70315953d..160cc26405 100644
--- a/packages/excalidraw/components/LibraryMenuItems.tsx
+++ b/packages/excalidraw/components/LibraryMenuItems.tsx
@@ -166,7 +166,7 @@ export default function LibraryMenuItems({
type: "everything",
elements: item.elements,
randomizeSeed: true,
- }).newElements,
+ }).duplicatedElements,
};
});
},
diff --git a/packages/excalidraw/components/Range.scss b/packages/excalidraw/components/Range.scss
index 01cb916897..8dcc705fea 100644
--- a/packages/excalidraw/components/Range.scss
+++ b/packages/excalidraw/components/Range.scss
@@ -6,7 +6,7 @@
.range-wrapper {
position: relative;
padding-top: 10px;
- padding-bottom: 30px;
+ padding-bottom: 25px;
}
.range-input {
diff --git a/packages/excalidraw/components/Stats/Angle.tsx b/packages/excalidraw/components/Stats/Angle.tsx
index 67693551f6..d0cb187dac 100644
--- a/packages/excalidraw/components/Stats/Angle.tsx
+++ b/packages/excalidraw/components/Stats/Angle.tsx
@@ -1,7 +1,5 @@
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
-import { mutateElement } from "@excalidraw/element/mutateElement";
-
import { getBoundTextElement } from "@excalidraw/element/textElement";
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
@@ -9,13 +7,14 @@ import type { Degrees } from "@excalidraw/math";
import type { ExcalidrawElement } from "@excalidraw/element/types";
+import type Scene from "@excalidraw/element/Scene";
+
import { angleIcon } from "../icons";
import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface AngleProps {
@@ -35,7 +34,6 @@ const handleDegreeChange: DragInputCallbackType = ({
scene,
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
- const elements = scene.getNonDeletedElements();
const origElement = originalElements[0];
if (origElement && !isElbowArrow(origElement)) {
const latestElement = elementsMap.get(origElement.id);
@@ -45,14 +43,14 @@ const handleDegreeChange: DragInputCallbackType = ({
if (nextValue !== undefined) {
const nextAngle = degreesToRadians(nextValue as Degrees);
- mutateElement(latestElement, {
+ scene.mutateElement(latestElement, {
angle: nextAngle,
});
- updateBindings(latestElement, elementsMap, elements, scene);
+ updateBindings(latestElement, scene);
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) {
- mutateElement(boundTextElement, { angle: nextAngle });
+ scene.mutateElement(boundTextElement, { angle: nextAngle });
}
return;
@@ -71,14 +69,14 @@ const handleDegreeChange: DragInputCallbackType = ({
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
- mutateElement(latestElement, {
+ scene.mutateElement(latestElement, {
angle: nextAngle,
});
- updateBindings(latestElement, elementsMap, elements, scene);
+ updateBindings(latestElement, scene);
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) {
- mutateElement(boundTextElement, { angle: nextAngle });
+ scene.mutateElement(boundTextElement, { angle: nextAngle });
}
}
};
diff --git a/packages/excalidraw/components/Stats/CanvasGrid.tsx b/packages/excalidraw/components/Stats/CanvasGrid.tsx
index 4611365f43..4766f82041 100644
--- a/packages/excalidraw/components/Stats/CanvasGrid.tsx
+++ b/packages/excalidraw/components/Stats/CanvasGrid.tsx
@@ -1,9 +1,10 @@
+import type Scene from "@excalidraw/element/Scene";
+
import { getNormalizedGridStep } from "../../scene";
import StatsDragInput from "./DragInput";
import { getStepSizedValue } from "./utils";
-import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface PositionProps {
diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx
index 142abc4074..c838b581f7 100644
--- a/packages/excalidraw/components/Stats/Dimension.tsx
+++ b/packages/excalidraw/components/Stats/Dimension.tsx
@@ -5,17 +5,17 @@ import {
MINIMAL_CROP_SIZE,
getUncroppedWidthAndHeight,
} from "@excalidraw/element/cropElement";
-import { mutateElement } from "@excalidraw/element/mutateElement";
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
import { isImageElement } from "@excalidraw/element/typeChecks";
import type { ExcalidrawElement } from "@excalidraw/element/types";
+import type Scene from "@excalidraw/element/Scene";
+
import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
interface DimensionDragInputProps {
@@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
};
}
- mutateElement(element, {
+ scene.mutateElement(element, {
crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
@@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
height: nextCropHeight,
};
- mutateElement(element, {
+ scene.mutateElement(element, {
crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
@@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight,
latestElement,
origElement,
- elementsMap,
originalElementsMap,
+ scene,
property === "width" ? "e" : "s",
{
shouldMaintainAspectRatio: keepAspectRatio,
@@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight,
latestElement,
origElement,
- elementsMap,
originalElementsMap,
+ scene,
property === "width" ? "e" : "s",
{
shouldMaintainAspectRatio: keepAspectRatio,
diff --git a/packages/excalidraw/components/Stats/DragInput.scss b/packages/excalidraw/components/Stats/DragInput.scss
index 76b9d147b1..f31616d949 100644
--- a/packages/excalidraw/components/Stats/DragInput.scss
+++ b/packages/excalidraw/components/Stats/DragInput.scss
@@ -2,10 +2,12 @@
.drag-input-container {
display: flex;
width: 100%;
+ border-radius: var(--border-radius-lg);
&:focus-within {
box-shadow: 0 0 0 1px var(--color-primary-darkest);
border-radius: var(--border-radius-md);
+ background: transparent;
}
}
@@ -16,24 +18,14 @@
.drag-input-label {
flex-shrink: 0;
- border: 1px solid var(--default-border-color);
- border-right: 0;
- padding: 0 0.5rem 0 0.75rem;
+ border: 0;
+ padding: 0 0.5rem 0 0.25rem;
min-width: 1rem;
+ width: 1.5rem;
height: 2rem;
- box-sizing: border-box;
+ box-sizing: content-box;
color: var(--popup-text-color);
- :root[dir="ltr"] & {
- border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
- }
-
- :root[dir="rtl"] & {
- border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
- border-right: 1px solid var(--default-border-color);
- border-left: 0;
- }
-
display: flex;
align-items: center;
justify-content: center;
@@ -51,20 +43,8 @@
border: 0;
outline: none;
height: 2rem;
- border: 1px solid var(--default-border-color);
- border-left: 0;
letter-spacing: 0.4px;
- :root[dir="ltr"] & {
- border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
- }
-
- :root[dir="rtl"] & {
- border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
- border-left: 1px solid var(--default-border-color);
- border-right: 0;
- }
-
padding: 0.5rem;
padding-left: 0.25rem;
appearance: none;
diff --git a/packages/excalidraw/components/Stats/DragInput.tsx b/packages/excalidraw/components/Stats/DragInput.tsx
index b4795308d8..6fdf909b24 100644
--- a/packages/excalidraw/components/Stats/DragInput.tsx
+++ b/packages/excalidraw/components/Stats/DragInput.tsx
@@ -7,6 +7,8 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
+import type Scene from "@excalidraw/element/Scene";
+
import { CaptureUpdateAction } from "../../store";
import { useApp } from "../App";
import { InlineIcon } from "../InlineIcon";
@@ -16,7 +18,6 @@ import { SMALLEST_DELTA } from "./utils";
import "./DragInput.scss";
import type { StatsInputProperty } from "./utils";
-import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
export type DragInputCallbackType<
@@ -216,13 +217,12 @@ const StatsDragInput = <
y: number;
} | null = null;
- let originalElementsMap: Map