wip
This commit is contained in:
parent
508f16dc04
commit
0458834681
@ -1,5 +1,90 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import type { Merge } from "./utility-types";
|
import type { Merge } from "./utility-types";
|
||||||
|
import { clamp } from "../math/utils";
|
||||||
|
import tinycolor from "tinycolor2";
|
||||||
|
import { degreesToRadians } from "../math/angle";
|
||||||
|
import type { Degrees } from "../math/types";
|
||||||
|
|
||||||
|
function cssHueRotate(
|
||||||
|
red: number,
|
||||||
|
green: number,
|
||||||
|
blue: number,
|
||||||
|
degrees: Degrees,
|
||||||
|
): { r: number; g: number; b: number } {
|
||||||
|
// normalize
|
||||||
|
const r = red / 255;
|
||||||
|
const g = green / 255;
|
||||||
|
const b = blue / 255;
|
||||||
|
|
||||||
|
// Convert degrees to radians
|
||||||
|
const a = degreesToRadians(degrees);
|
||||||
|
|
||||||
|
const c = Math.cos(a);
|
||||||
|
const s = Math.sin(a);
|
||||||
|
|
||||||
|
// rotation matrix
|
||||||
|
const matrix = [
|
||||||
|
0.213 + c * 0.787 - s * 0.213,
|
||||||
|
0.715 - c * 0.715 - s * 0.715,
|
||||||
|
0.072 - c * 0.072 + s * 0.928,
|
||||||
|
0.213 - c * 0.213 + s * 0.143,
|
||||||
|
0.715 + c * 0.285 + s * 0.14,
|
||||||
|
0.072 - c * 0.072 - s * 0.283,
|
||||||
|
0.213 - c * 0.213 - s * 0.787,
|
||||||
|
0.715 - c * 0.715 + s * 0.715,
|
||||||
|
0.072 + c * 0.928 + s * 0.072,
|
||||||
|
];
|
||||||
|
|
||||||
|
// transform
|
||||||
|
const newR = r * matrix[0] + g * matrix[1] + b * matrix[2];
|
||||||
|
const newG = r * matrix[3] + g * matrix[4] + b * matrix[5];
|
||||||
|
const newB = r * matrix[6] + g * matrix[7] + b * matrix[8];
|
||||||
|
|
||||||
|
// clamp the values to [0, 1] range and convert back to [0, 255]
|
||||||
|
return {
|
||||||
|
r: Math.round(Math.max(0, Math.min(1, newR)) * 255),
|
||||||
|
g: Math.round(Math.max(0, Math.min(1, newG)) * 255),
|
||||||
|
b: Math.round(Math.max(0, Math.min(1, newB)) * 255),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssInvert = (
|
||||||
|
r: number,
|
||||||
|
g: number,
|
||||||
|
b: number,
|
||||||
|
percent: number,
|
||||||
|
): { r: number; g: number; b: number } => {
|
||||||
|
const p = clamp(percent, 0, 100) / 100;
|
||||||
|
|
||||||
|
// Function to invert a single color component
|
||||||
|
const invertComponent = (color: number): number => {
|
||||||
|
// Apply the invert formula
|
||||||
|
const inverted = color * (1 - p) + (255 - color) * p;
|
||||||
|
// Round to the nearest integer and clamp to [0, 255]
|
||||||
|
return Math.round(clamp(inverted, 0, 255));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the inverted RGB components
|
||||||
|
const invertedR = invertComponent(r);
|
||||||
|
const invertedG = invertComponent(g);
|
||||||
|
const invertedB = invertComponent(b);
|
||||||
|
|
||||||
|
return { r: invertedR, g: invertedG, b: invertedB };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyDarkModeFilter = (color: string) => {
|
||||||
|
let tc = tinycolor(color);
|
||||||
|
|
||||||
|
const _alpha = tc._a;
|
||||||
|
|
||||||
|
// order of operations matters
|
||||||
|
// (corresponds to "filter: invert(invertPercent) hue-rotate(hueDegrees)" in css)
|
||||||
|
tc = tinycolor(cssInvert(tc._r, tc._g, tc._b, 93));
|
||||||
|
tc = tinycolor(cssHueRotate(tc._r, tc._g, tc._b, 180 as Degrees));
|
||||||
|
tc.setAlpha(_alpha);
|
||||||
|
|
||||||
|
return tc.toHex8String();
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME can't put to utils.ts rn because of circular dependency
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
|
@ -1700,6 +1700,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsPendingErasure: this.elementsPendingErasure,
|
elementsPendingErasure: this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes:
|
pendingFlowchartNodes:
|
||||||
this.flowChartCreator.pendingNodes,
|
this.flowChartCreator.pendingNodes,
|
||||||
|
theme: this.state.theme,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{this.state.newElement && (
|
{this.state.newElement && (
|
||||||
@ -1720,6 +1721,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsPendingErasure:
|
elementsPendingErasure:
|
||||||
this.elementsPendingErasure,
|
this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes: null,
|
pendingFlowchartNodes: null,
|
||||||
|
theme: this.state.theme,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -2695,6 +2697,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (prevState.theme !== this.state.theme) {
|
||||||
|
this.scene
|
||||||
|
.getElementsIncludingDeleted()
|
||||||
|
.forEach((element) => ShapeCache.delete(element));
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.state.activeTool.type === "eraser" &&
|
this.state.activeTool.type === "eraser" &&
|
||||||
prevState.theme !== this.state.theme
|
prevState.theme !== this.state.theme
|
||||||
|
@ -124,7 +124,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
// recommends surface color of #121212, 93% yields #111111 for #FFF
|
// recommends surface color of #121212, 93% yields #111111 for #FFF
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
filter: var(--theme-filter);
|
// filter: var(--theme-filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +189,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
$theme-filter: "invert(93%) hue-rotate(180deg)"; // prod
|
||||||
|
// $theme-filter: "invert(93%)"; // prod
|
||||||
|
// $theme-filter: "hue-rotate(180deg)"; // prod
|
||||||
|
// $theme-filter: "hue-rotate(180deg) invert(93%)";
|
||||||
|
|
||||||
$right-sidebar-width: "302px";
|
$right-sidebar-width: "302px";
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
|
import { CLASSES, isSafari, POINTER_BUTTON, THEME } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -50,6 +50,7 @@ import {
|
|||||||
originalContainerCache,
|
originalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@ -273,10 +274,15 @@ export const textWysiwyg = ({
|
|||||||
textAlign,
|
textAlign,
|
||||||
verticalAlign,
|
verticalAlign,
|
||||||
color: updatedTextElement.strokeColor,
|
color: updatedTextElement.strokeColor,
|
||||||
|
// color:
|
||||||
|
// appState.theme === THEME.DARK
|
||||||
|
// ? applyDarkModeFilter(updatedTextElement.strokeColor)
|
||||||
|
// : updatedTextElement.strokeColor,
|
||||||
opacity: updatedTextElement.opacity / 100,
|
opacity: updatedTextElement.opacity / 100,
|
||||||
filter: "var(--theme-filter)",
|
filter: "var(--theme-filter)",
|
||||||
maxHeight: `${editorMaxHeight}px`,
|
maxHeight: `${editorMaxHeight}px`,
|
||||||
});
|
});
|
||||||
|
// console.log("...", updatedTextElement.strokeColor);
|
||||||
editable.scrollTop = 0;
|
editable.scrollTop = 0;
|
||||||
// For some reason updating font attribute doesn't set font family
|
// For some reason updating font attribute doesn't set font family
|
||||||
// hence updating font family explicitly for test environment
|
// hence updating font family explicitly for test environment
|
||||||
|
10
packages/excalidraw/global.d.ts
vendored
10
packages/excalidraw/global.d.ts
vendored
@ -104,3 +104,13 @@ declare namespace jest {
|
|||||||
toBeNonNaNNumber(): void;
|
toBeNonNaNNumber(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare namespace tinycolor {
|
||||||
|
interface Instance {
|
||||||
|
_r: number;
|
||||||
|
_g: number;
|
||||||
|
_b: number;
|
||||||
|
_a: number;
|
||||||
|
_ok: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@tldraw/vec": "1.7.1",
|
"@tldraw/vec": "1.7.1",
|
||||||
|
"@types/tinycolor2": "1.4.6",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
@ -84,6 +85,7 @@
|
|||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"roughjs": "4.6.4",
|
"roughjs": "4.6.4",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
|
"tinycolor2": "1.6.0",
|
||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -3,6 +3,7 @@ import type { StaticCanvasAppState, AppState } from "../types";
|
|||||||
import type { StaticCanvasRenderConfig } from "../scene/types";
|
import type { StaticCanvasRenderConfig } from "../scene/types";
|
||||||
|
|
||||||
import { THEME, THEME_FILTER } from "../constants";
|
import { THEME, THEME_FILTER } from "../constants";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
export const fillCircle = (
|
export const fillCircle = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@ -50,7 +51,7 @@ export const bootstrapCanvas = ({
|
|||||||
context.scale(scale, scale);
|
context.scale(scale, scale);
|
||||||
|
|
||||||
if (isExporting && theme === THEME.DARK) {
|
if (isExporting && theme === THEME.DARK) {
|
||||||
context.filter = THEME_FILTER;
|
// context.filter = THEME_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint background
|
// Paint background
|
||||||
@ -64,7 +65,10 @@ export const bootstrapCanvas = ({
|
|||||||
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
|
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
|
||||||
}
|
}
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle = viewBackgroundColor;
|
context.fillStyle =
|
||||||
|
theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(viewBackgroundColor)
|
||||||
|
: viewBackgroundColor;
|
||||||
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
|
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,6 +61,7 @@ import { ShapeCache } from "../scene/ShapeCache";
|
|||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { isRightAngleRads } from "../../math";
|
import { isRightAngleRads } from "../../math";
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "../shapes";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@ -247,9 +248,9 @@ const generateElementCanvas = (
|
|||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
|
|
||||||
// in dark theme, revert the image color filter
|
// in dark theme, revert the image color filter
|
||||||
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
// if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||||
context.filter = IMAGE_INVERT_FILTER;
|
// context.filter = IMAGE_INVERT_FILTER;
|
||||||
}
|
// }
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
|
|
||||||
@ -403,7 +404,10 @@ const drawElementOnCanvas = (
|
|||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
// Draw directly to canvas
|
// Draw directly to canvas
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor;
|
||||||
|
|
||||||
const path = getFreeDrawPath2D(element) as Path2D;
|
const path = getFreeDrawPath2D(element) as Path2D;
|
||||||
const fillShape = ShapeCache.get(element);
|
const fillShape = ShapeCache.get(element);
|
||||||
@ -412,7 +416,10 @@ const drawElementOnCanvas = (
|
|||||||
rc.draw(fillShape);
|
rc.draw(fillShape);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor;
|
||||||
context.fill(path);
|
context.fill(path);
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
@ -458,7 +465,10 @@ const drawElementOnCanvas = (
|
|||||||
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
||||||
context.save();
|
context.save();
|
||||||
context.font = getFontString(element);
|
context.font = getFontString(element);
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor;
|
||||||
context.textAlign = element.textAlign as CanvasTextAlign;
|
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
// Canvas does not support multiline text by default
|
// Canvas does not support multiline text by default
|
||||||
@ -699,7 +709,10 @@ export const renderElement = (
|
|||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||||
context.strokeStyle = FRAME_STYLE.strokeColor;
|
context.strokeStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: FRAME_STYLE.strokeColor;
|
||||||
|
|
||||||
// TODO change later to only affect AI frames
|
// TODO change later to only affect AI frames
|
||||||
if (isMagicFrameElement(element)) {
|
if (isMagicFrameElement(element)) {
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
|
THEME,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { normalizeLink, toValidURL } from "../data/url";
|
import { normalizeLink, toValidURL } from "../data/url";
|
||||||
import { getElementAbsoluteCoords } from "../element";
|
import { getElementAbsoluteCoords } from "../element";
|
||||||
@ -37,6 +38,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
|||||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const roughSVGDrawWithPrecision = (
|
const roughSVGDrawWithPrecision = (
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
@ -139,7 +141,7 @@ const renderElementToSvg = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
const shape = ShapeCache.generateElementShape(element, null);
|
const shape = ShapeCache.generateElementShape(element, renderConfig);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
shape,
|
shape,
|
||||||
@ -389,7 +391,12 @@ const renderElementToSvg = (
|
|||||||
);
|
);
|
||||||
node.setAttribute("stroke", "none");
|
node.setAttribute("stroke", "none");
|
||||||
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
||||||
path.setAttribute("fill", element.strokeColor);
|
path.setAttribute(
|
||||||
|
"fill",
|
||||||
|
renderConfig.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
|
);
|
||||||
path.setAttribute("d", getFreeDrawSvgPath(element));
|
path.setAttribute("d", getFreeDrawSvgPath(element));
|
||||||
node.appendChild(path);
|
node.appendChild(path);
|
||||||
|
|
||||||
@ -526,7 +533,12 @@ const renderElementToSvg = (
|
|||||||
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
||||||
|
|
||||||
rect.setAttribute("fill", "none");
|
rect.setAttribute("fill", "none");
|
||||||
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
|
rect.setAttribute(
|
||||||
|
"stroke",
|
||||||
|
renderConfig.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(FRAME_STYLE.strokeColor)
|
||||||
|
: FRAME_STYLE.strokeColor,
|
||||||
|
);
|
||||||
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
||||||
|
|
||||||
addToRoot(rect, element);
|
addToRoot(rect, element);
|
||||||
@ -577,7 +589,12 @@ const renderElementToSvg = (
|
|||||||
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
|
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
|
||||||
text.setAttribute("font-family", getFontFamilyString(element));
|
text.setAttribute("font-family", getFontFamilyString(element));
|
||||||
text.setAttribute("font-size", `${element.fontSize}px`);
|
text.setAttribute("font-size", `${element.fontSize}px`);
|
||||||
text.setAttribute("fill", element.strokeColor);
|
text.setAttribute(
|
||||||
|
"fill",
|
||||||
|
renderConfig.theme === THEME.DARK
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
|
);
|
||||||
text.setAttribute("text-anchor", textAnchor);
|
text.setAttribute("text-anchor", textAnchor);
|
||||||
text.setAttribute("style", "white-space: pre;");
|
text.setAttribute("style", "white-space: pre;");
|
||||||
text.setAttribute("direction", direction);
|
text.setAttribute("direction", direction);
|
||||||
|
@ -13,7 +13,7 @@ import type {
|
|||||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
import { generateFreeDrawShape } from "../renderer/renderElement";
|
||||||
import { isTransparent, assertNever } from "../utils";
|
import { isTransparent, assertNever } from "../utils";
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { ROUGHNESS } from "../constants";
|
import { ROUGHNESS, THEME } from "../constants";
|
||||||
import {
|
import {
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import type { EmbedsValidationStatus } from "../types";
|
import type { AppState, EmbedsValidationStatus } from "../types";
|
||||||
import {
|
import {
|
||||||
point,
|
point,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
@ -30,6 +30,7 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
@ -61,6 +62,7 @@ function adjustRoughness(element: ExcalidrawElement): number {
|
|||||||
export const generateRoughOptions = (
|
export const generateRoughOptions = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
continuousPath = false,
|
continuousPath = false,
|
||||||
|
isDarkMode: boolean = false,
|
||||||
): Options => {
|
): Options => {
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
seed: element.seed,
|
seed: element.seed,
|
||||||
@ -85,7 +87,9 @@ export const generateRoughOptions = (
|
|||||||
fillWeight: element.strokeWidth / 2,
|
fillWeight: element.strokeWidth / 2,
|
||||||
hachureGap: element.strokeWidth * 4,
|
hachureGap: element.strokeWidth * 4,
|
||||||
roughness: adjustRoughness(element),
|
roughness: adjustRoughness(element),
|
||||||
stroke: element.strokeColor,
|
stroke: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
preserveVertices:
|
preserveVertices:
|
||||||
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
||||||
};
|
};
|
||||||
@ -99,6 +103,8 @@ export const generateRoughOptions = (
|
|||||||
options.fillStyle = element.fillStyle;
|
options.fillStyle = element.fillStyle;
|
||||||
options.fill = isTransparent(element.backgroundColor)
|
options.fill = isTransparent(element.backgroundColor)
|
||||||
? undefined
|
? undefined
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.backgroundColor)
|
||||||
: element.backgroundColor;
|
: element.backgroundColor;
|
||||||
if (element.type === "ellipse") {
|
if (element.type === "ellipse") {
|
||||||
options.curveFitting = 1;
|
options.curveFitting = 1;
|
||||||
@ -112,6 +118,8 @@ export const generateRoughOptions = (
|
|||||||
options.fill =
|
options.fill =
|
||||||
element.backgroundColor === "transparent"
|
element.backgroundColor === "transparent"
|
||||||
? undefined
|
? undefined
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.backgroundColor)
|
||||||
: element.backgroundColor;
|
: element.backgroundColor;
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
@ -165,6 +173,7 @@ const getArrowheadShapes = (
|
|||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
options: Options,
|
options: Options,
|
||||||
canvasBackgroundColor: string,
|
canvasBackgroundColor: string,
|
||||||
|
isDarkMode: boolean,
|
||||||
) => {
|
) => {
|
||||||
const arrowheadPoints = getArrowheadPoints(
|
const arrowheadPoints = getArrowheadPoints(
|
||||||
element,
|
element,
|
||||||
@ -192,10 +201,14 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "circle_outline"
|
arrowhead === "circle_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
|
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
stroke: element.strokeColor,
|
stroke: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
|
: element.strokeColor,
|
||||||
roughness: Math.min(0.5, options.roughness || 0),
|
roughness: Math.min(0.5, options.roughness || 0),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@ -220,6 +233,8 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "triangle_outline"
|
arrowhead === "triangle_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
roughness: Math.min(1, options.roughness || 0),
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
@ -248,6 +263,8 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "diamond_outline"
|
arrowhead === "diamond_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
|
: isDarkMode
|
||||||
|
? applyDarkModeFilter(element.strokeColor)
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
roughness: Math.min(1, options.roughness || 0),
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
@ -291,12 +308,15 @@ export const _generateElementShape = (
|
|||||||
isExporting,
|
isExporting,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
|
theme,
|
||||||
}: {
|
}: {
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
canvasBackgroundColor: string;
|
canvasBackgroundColor: string;
|
||||||
embedsValidationStatus: EmbedsValidationStatus | null;
|
embedsValidationStatus: EmbedsValidationStatus | null;
|
||||||
|
theme: AppState["theme"];
|
||||||
},
|
},
|
||||||
): Drawable | Drawable[] | null => {
|
): Drawable | Drawable[] | null => {
|
||||||
|
const isDarkMode = theme === THEME.DARK;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
@ -322,6 +342,7 @@ export const _generateElementShape = (
|
|||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
|
isDarkMode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -337,6 +358,7 @@ export const _generateElementShape = (
|
|||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
),
|
),
|
||||||
false,
|
false,
|
||||||
|
isDarkMode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -374,7 +396,7 @@ export const _generateElementShape = (
|
|||||||
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
||||||
topY + horizontalRadius
|
topY + horizontalRadius
|
||||||
}`,
|
}`,
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(element, true, isDarkMode),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
shape = generator.polygon(
|
shape = generator.polygon(
|
||||||
@ -384,7 +406,7 @@ export const _generateElementShape = (
|
|||||||
[bottomX, bottomY],
|
[bottomX, bottomY],
|
||||||
[leftX, leftY],
|
[leftX, leftY],
|
||||||
],
|
],
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element, undefined, isDarkMode),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return shape;
|
return shape;
|
||||||
@ -395,14 +417,14 @@ export const _generateElementShape = (
|
|||||||
element.height / 2,
|
element.height / 2,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element, undefined, isDarkMode),
|
||||||
);
|
);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
case "line":
|
case "line":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
let shape: ElementShapes[typeof element.type];
|
let shape: ElementShapes[typeof element.type];
|
||||||
const options = generateRoughOptions(element);
|
const options = generateRoughOptions(element, undefined, isDarkMode);
|
||||||
|
|
||||||
// points array can be empty in the beginning, so it is important to add
|
// points array can be empty in the beginning, so it is important to add
|
||||||
// initial position to it
|
// initial position to it
|
||||||
@ -414,7 +436,7 @@ export const _generateElementShape = (
|
|||||||
shape = [
|
shape = [
|
||||||
generator.path(
|
generator.path(
|
||||||
generateElbowArrowShape(points, 16),
|
generateElbowArrowShape(points, 16),
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(element, true, isDarkMode),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
} else if (!element.roundness) {
|
} else if (!element.roundness) {
|
||||||
@ -446,6 +468,7 @@ export const _generateElementShape = (
|
|||||||
generator,
|
generator,
|
||||||
options,
|
options,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
|
isDarkMode,
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
@ -463,6 +486,7 @@ export const _generateElementShape = (
|
|||||||
generator,
|
generator,
|
||||||
options,
|
options,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
|
isDarkMode,
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
@ -477,7 +501,7 @@ export const _generateElementShape = (
|
|||||||
// generate rough polygon to fill freedraw shape
|
// generate rough polygon to fill freedraw shape
|
||||||
const simplifiedPoints = simplify(element.points, 0.75);
|
const simplifiedPoints = simplify(element.points, 0.75);
|
||||||
shape = generator.curve(simplifiedPoints as [number, number][], {
|
shape = generator.curve(simplifiedPoints as [number, number][], {
|
||||||
...generateRoughOptions(element),
|
...generateRoughOptions(element, undefined, isDarkMode),
|
||||||
stroke: "none",
|
stroke: "none",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -9,6 +9,7 @@ import { _generateElementShape } from "./Shape";
|
|||||||
import type { ElementShape, ElementShapes } from "./types";
|
import type { ElementShape, ElementShapes } from "./types";
|
||||||
import { COLOR_PALETTE } from "../colors";
|
import { COLOR_PALETTE } from "../colors";
|
||||||
import type { AppState, EmbedsValidationStatus } from "../types";
|
import type { AppState, EmbedsValidationStatus } from "../types";
|
||||||
|
import { THEME } from "..";
|
||||||
|
|
||||||
export class ShapeCache {
|
export class ShapeCache {
|
||||||
private static rg = new RoughGenerator();
|
private static rg = new RoughGenerator();
|
||||||
@ -52,6 +53,7 @@ export class ShapeCache {
|
|||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
|
theme: AppState["theme"];
|
||||||
} | null,
|
} | null,
|
||||||
) => {
|
) => {
|
||||||
// when exporting, always regenerated to guarantee the latest shape
|
// when exporting, always regenerated to guarantee the latest shape
|
||||||
@ -74,6 +76,7 @@ export class ShapeCache {
|
|||||||
isExporting: false,
|
isExporting: false,
|
||||||
canvasBackgroundColor: COLOR_PALETTE.white,
|
canvasBackgroundColor: COLOR_PALETTE.white,
|
||||||
embedsValidationStatus: null,
|
embedsValidationStatus: null,
|
||||||
|
theme: THEME.LIGHT,
|
||||||
},
|
},
|
||||||
) as T["type"] extends keyof ElementShapes
|
) as T["type"] extends keyof ElementShapes
|
||||||
? ElementShapes[T["type"]]
|
? ElementShapes[T["type"]]
|
||||||
|
@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex";
|
|||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import type { Font } from "../fonts/ExcalidrawFont";
|
import type { Font } from "../fonts/ExcalidrawFont";
|
||||||
|
import { applyDarkModeFilter } from "../colors";
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@ -214,6 +215,8 @@ export const exportToCanvas = async (
|
|||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
|
||||||
|
|
||||||
renderStaticScene({
|
renderStaticScene({
|
||||||
canvas,
|
canvas,
|
||||||
rc: rough.canvas(canvas),
|
rc: rough.canvas(canvas),
|
||||||
@ -233,7 +236,7 @@ export const exportToCanvas = async (
|
|||||||
scrollY: -minY + exportPadding,
|
scrollY: -minY + exportPadding,
|
||||||
zoom: defaultAppState.zoom,
|
zoom: defaultAppState.zoom,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
|
theme,
|
||||||
},
|
},
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
canvasBackgroundColor: viewBackgroundColor,
|
canvasBackgroundColor: viewBackgroundColor,
|
||||||
@ -244,6 +247,7 @@ export const exportToCanvas = async (
|
|||||||
embedsValidationStatus: new Map(),
|
embedsValidationStatus: new Map(),
|
||||||
elementsPendingErasure: new Set(),
|
elementsPendingErasure: new Set(),
|
||||||
pendingFlowchartNodes: null,
|
pendingFlowchartNodes: null,
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -330,7 +334,7 @@ export const exportToSvg = async (
|
|||||||
svgRoot.setAttribute("width", `${width * exportScale}`);
|
svgRoot.setAttribute("width", `${width * exportScale}`);
|
||||||
svgRoot.setAttribute("height", `${height * exportScale}`);
|
svgRoot.setAttribute("height", `${height * exportScale}`);
|
||||||
if (exportWithDarkMode) {
|
if (exportWithDarkMode) {
|
||||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
// svgRoot.setAttribute("filter", THEME_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetX = -minX + exportPadding;
|
const offsetX = -minX + exportPadding;
|
||||||
@ -376,7 +380,12 @@ export const exportToSvg = async (
|
|||||||
rect.setAttribute("y", "0");
|
rect.setAttribute("y", "0");
|
||||||
rect.setAttribute("width", `${width}`);
|
rect.setAttribute("width", `${width}`);
|
||||||
rect.setAttribute("height", `${height}`);
|
rect.setAttribute("height", `${height}`);
|
||||||
rect.setAttribute("fill", viewBackgroundColor);
|
rect.setAttribute(
|
||||||
|
"fill",
|
||||||
|
appState.exportWithDarkMode
|
||||||
|
? applyDarkModeFilter(viewBackgroundColor)
|
||||||
|
: viewBackgroundColor,
|
||||||
|
);
|
||||||
svgRoot.appendChild(rect);
|
svgRoot.appendChild(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,6 +393,8 @@ export const exportToSvg = async (
|
|||||||
|
|
||||||
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
||||||
|
|
||||||
|
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
|
||||||
|
|
||||||
renderSceneToSvg(
|
renderSceneToSvg(
|
||||||
elementsForRender,
|
elementsForRender,
|
||||||
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
||||||
@ -405,6 +416,7 @@ export const exportToSvg = async (
|
|||||||
.map((element) => [element.id, true]),
|
.map((element) => [element.id, true]),
|
||||||
)
|
)
|
||||||
: new Map(),
|
: new Map(),
|
||||||
|
theme,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ export type StaticCanvasRenderConfig = {
|
|||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
elementsPendingErasure: ElementsPendingErasure;
|
elementsPendingErasure: ElementsPendingErasure;
|
||||||
pendingFlowchartNodes: PendingExcalidrawElements | null;
|
pendingFlowchartNodes: PendingExcalidrawElements | null;
|
||||||
|
theme: AppState["theme"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SVGRenderConfig = {
|
export type SVGRenderConfig = {
|
||||||
@ -46,6 +47,7 @@ export type SVGRenderConfig = {
|
|||||||
frameRendering: AppState["frameRendering"];
|
frameRendering: AppState["frameRendering"];
|
||||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
|
theme: AppState["theme"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InteractiveCanvasRenderConfig = {
|
export type InteractiveCanvasRenderConfig = {
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -3299,6 +3299,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/jest" "*"
|
"@types/jest" "*"
|
||||||
|
|
||||||
|
"@types/tinycolor2@1.4.6":
|
||||||
|
version "1.4.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz#670cbc0caf4e58dd61d1e3a6f26386e473087f06"
|
||||||
|
integrity sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==
|
||||||
|
|
||||||
"@types/trusted-types@^2.0.2":
|
"@types/trusted-types@^2.0.2":
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
@ -9976,6 +9981,11 @@ tinybench@^2.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
|
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
|
||||||
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
|
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
|
||||||
|
|
||||||
|
tinycolor2@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
|
||||||
|
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
|
||||||
|
|
||||||
tinypool@^1.0.0:
|
tinypool@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"
|
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user