Compare commits

...

1 Commits

Author SHA1 Message Date
dwelle
0458834681 wip 2024-09-16 10:49:08 +02:00
15 changed files with 228 additions and 29 deletions

View File

@ -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)[]>(

View File

@ -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

View File

@ -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);
} }
} }

View File

@ -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 {

View File

@ -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

View File

@ -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;
}
}

View File

@ -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": {

View File

@ -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 {

View File

@ -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)) {

View File

@ -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);

View File

@ -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 {

View File

@ -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"]]

View File

@ -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,
}, },
); );

View File

@ -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 = {

View File

@ -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"