fix: export scale quality regression (#4316)
This commit is contained in:
parent
f9d2d537a2
commit
8ff159e76e
@ -174,7 +174,7 @@ import {
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { SceneState, ScrollBars } from "../scene/types";
|
||||
import { RenderConfig, ScrollBars } from "../scene/types";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import {
|
||||
@ -1053,8 +1053,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const cursorButton: {
|
||||
[id: string]: string | undefined;
|
||||
} = {};
|
||||
const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
|
||||
const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
|
||||
const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] =
|
||||
{};
|
||||
const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] =
|
||||
{};
|
||||
const pointerUsernames: { [id: string]: string } = {};
|
||||
const pointerUserStates: { [id: string]: string } = {};
|
||||
this.state.collaborators.forEach((user, socketId) => {
|
||||
@ -1122,9 +1124,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
||||
theme: this.state.theme,
|
||||
imageCache: this.imageCache,
|
||||
},
|
||||
{
|
||||
renderOptimizations: true,
|
||||
isExporting: false,
|
||||
renderScrollbars: !this.isMobile,
|
||||
},
|
||||
);
|
||||
|
@ -22,7 +22,7 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { Drawable, Options } from "roughjs/bin/core";
|
||||
import { RoughSVG } from "roughjs/bin/svg";
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
import { SceneState } from "../scene/types";
|
||||
import { RenderConfig } from "../scene/types";
|
||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
||||
import { isPathALoop } from "../math";
|
||||
import rough from "roughjs/bin/rough";
|
||||
@ -41,10 +41,22 @@ const defaultAppState = getDefaultAppState();
|
||||
|
||||
const isPendingImageElement = (
|
||||
element: ExcalidrawElement,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
) =>
|
||||
isInitializedImageElement(element) &&
|
||||
!sceneState.imageCache.has(element.fileId);
|
||||
!renderConfig.imageCache.has(element.fileId);
|
||||
|
||||
const shouldResetImageFilter = (
|
||||
element: ExcalidrawElement,
|
||||
renderConfig: RenderConfig,
|
||||
) => {
|
||||
return (
|
||||
renderConfig.theme === "dark" &&
|
||||
isInitializedImageElement(element) &&
|
||||
!isPendingImageElement(element, renderConfig) &&
|
||||
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
||||
);
|
||||
};
|
||||
|
||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||
|
||||
@ -56,7 +68,7 @@ const getCanvasPadding = (element: ExcalidrawElement) =>
|
||||
export interface ExcalidrawElementWithCanvas {
|
||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||
canvas: HTMLCanvasElement;
|
||||
theme: SceneState["theme"];
|
||||
theme: RenderConfig["theme"];
|
||||
canvasZoom: Zoom["value"];
|
||||
canvasOffsetX: number;
|
||||
canvasOffsetY: number;
|
||||
@ -65,7 +77,7 @@ export interface ExcalidrawElementWithCanvas {
|
||||
const generateElementCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
zoom: Zoom,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
): ExcalidrawElementWithCanvas => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d")!;
|
||||
@ -123,22 +135,17 @@ const generateElementCanvas = (
|
||||
const rc = rough.canvas(canvas);
|
||||
|
||||
// in dark theme, revert the image color filter
|
||||
if (
|
||||
sceneState.theme === "dark" &&
|
||||
isInitializedImageElement(element) &&
|
||||
!isPendingImageElement(element, sceneState) &&
|
||||
sceneState.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
||||
) {
|
||||
if (shouldResetImageFilter(element, renderConfig)) {
|
||||
context.filter = IMAGE_INVERT_FILTER;
|
||||
}
|
||||
|
||||
drawElementOnCanvas(element, rc, context, sceneState);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
context.restore();
|
||||
|
||||
return {
|
||||
element,
|
||||
canvas,
|
||||
theme: sceneState.theme,
|
||||
theme: renderConfig.theme,
|
||||
canvasZoom: zoom.value,
|
||||
canvasOffsetX,
|
||||
canvasOffsetY,
|
||||
@ -185,7 +192,7 @@ const drawElementOnCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
) => {
|
||||
context.globalAlpha = element.opacity / 100;
|
||||
switch (element.type) {
|
||||
@ -222,7 +229,7 @@ const drawElementOnCanvas = (
|
||||
}
|
||||
case "image": {
|
||||
const img = isInitializedImageElement(element)
|
||||
? sceneState.imageCache.get(element.fileId)?.image
|
||||
? renderConfig.imageCache.get(element.fileId)?.image
|
||||
: undefined;
|
||||
if (img != null && !(img instanceof Promise)) {
|
||||
context.drawImage(
|
||||
@ -233,7 +240,7 @@ const drawElementOnCanvas = (
|
||||
element.height,
|
||||
);
|
||||
} else {
|
||||
drawImagePlaceholder(element, context, sceneState.zoom.value);
|
||||
drawImagePlaceholder(element, context, renderConfig.zoom.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -566,21 +573,25 @@ const generateElementShape = (
|
||||
|
||||
const generateElementWithCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
) => {
|
||||
const zoom: Zoom = sceneState ? sceneState.zoom : defaultAppState.zoom;
|
||||
const zoom: Zoom = renderConfig ? renderConfig.zoom : defaultAppState.zoom;
|
||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||
const shouldRegenerateBecauseZoom =
|
||||
prevElementWithCanvas &&
|
||||
prevElementWithCanvas.canvasZoom !== zoom.value &&
|
||||
!sceneState?.shouldCacheIgnoreZoom;
|
||||
!renderConfig?.shouldCacheIgnoreZoom;
|
||||
|
||||
if (
|
||||
!prevElementWithCanvas ||
|
||||
shouldRegenerateBecauseZoom ||
|
||||
prevElementWithCanvas.theme !== sceneState.theme
|
||||
prevElementWithCanvas.theme !== renderConfig.theme
|
||||
) {
|
||||
const elementWithCanvas = generateElementCanvas(element, zoom, sceneState);
|
||||
const elementWithCanvas = generateElementCanvas(
|
||||
element,
|
||||
zoom,
|
||||
renderConfig,
|
||||
);
|
||||
|
||||
elementWithCanvasCache.set(element, elementWithCanvas);
|
||||
|
||||
@ -593,7 +604,7 @@ const drawElementFromCanvas = (
|
||||
elementWithCanvas: ExcalidrawElementWithCanvas,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
) => {
|
||||
const element = elementWithCanvas.element;
|
||||
const padding = getCanvasPadding(element);
|
||||
@ -607,10 +618,10 @@ const drawElementFromCanvas = (
|
||||
y2 = Math.ceil(y2);
|
||||
}
|
||||
|
||||
const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
|
||||
const cy = ((y1 + y2) / 2 + sceneState.scrollY) * window.devicePixelRatio;
|
||||
const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio;
|
||||
const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio;
|
||||
|
||||
const _isPendingImageElement = isPendingImageElement(element, sceneState);
|
||||
const _isPendingImageElement = isPendingImageElement(element, renderConfig);
|
||||
|
||||
const scaleXFactor =
|
||||
"scale" in elementWithCanvas.element && !_isPendingImageElement
|
||||
@ -647,16 +658,15 @@ export const renderElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderOptimizations: boolean,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
) => {
|
||||
const generator = rc.generator;
|
||||
switch (element.type) {
|
||||
case "selection": {
|
||||
context.save();
|
||||
context.translate(
|
||||
element.x + sceneState.scrollX,
|
||||
element.y + sceneState.scrollY,
|
||||
element.x + renderConfig.scrollX,
|
||||
element.y + renderConfig.scrollY,
|
||||
);
|
||||
context.fillStyle = "rgba(0, 0, 255, 0.10)";
|
||||
context.fillRect(0, 0, element.width, element.height);
|
||||
@ -666,23 +676,23 @@ export const renderElement = (
|
||||
case "freedraw": {
|
||||
generateElementShape(element, generator);
|
||||
|
||||
if (renderOptimizations) {
|
||||
if (renderConfig.isExporting) {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
sceneState,
|
||||
renderConfig,
|
||||
);
|
||||
drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
|
||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
||||
} else {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2 + sceneState.scrollX;
|
||||
const cy = (y1 + y2) / 2 + sceneState.scrollY;
|
||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
||||
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(element.angle);
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(element, rc, context, sceneState);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
@ -696,24 +706,31 @@ export const renderElement = (
|
||||
case "image":
|
||||
case "text": {
|
||||
generateElementShape(element, generator);
|
||||
if (renderOptimizations) {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
sceneState,
|
||||
);
|
||||
drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
|
||||
} else {
|
||||
if (renderConfig.isExporting) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2 + sceneState.scrollX;
|
||||
const cy = (y1 + y2) / 2 + sceneState.scrollY;
|
||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
||||
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(element.angle);
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(element, rc, context, sceneState);
|
||||
|
||||
if (shouldResetImageFilter(element, renderConfig)) {
|
||||
context.filter = "none";
|
||||
}
|
||||
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
context.restore();
|
||||
// not exporting → optimized rendering (cache & render from element
|
||||
// canvases)
|
||||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
renderConfig,
|
||||
);
|
||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
} from "../element";
|
||||
|
||||
import { roundRect } from "./roundRect";
|
||||
import { SceneState } from "../scene/types";
|
||||
import { RenderConfig } from "../scene/types";
|
||||
import {
|
||||
getScrollBars,
|
||||
SCROLLBAR_COLOR,
|
||||
@ -146,12 +146,12 @@ const strokeGrid = (
|
||||
const renderLinearPointHandles = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: AppState,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(sceneState.scrollX, sceneState.scrollY);
|
||||
context.lineWidth = 1 / sceneState.zoom.value;
|
||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
||||
|
||||
LinearElementEditor.getPointsGlobalCoordinates(element).forEach(
|
||||
(point, idx) => {
|
||||
@ -166,7 +166,7 @@ const renderLinearPointHandles = (
|
||||
context,
|
||||
point[0],
|
||||
point[1],
|
||||
POINT_HANDLE_SIZE / 2 / sceneState.zoom.value,
|
||||
POINT_HANDLE_SIZE / 2 / renderConfig.zoom.value,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -180,31 +180,20 @@ export const renderScene = (
|
||||
scale: number,
|
||||
rc: RoughCanvas,
|
||||
canvas: HTMLCanvasElement,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
// extra options passed to the renderer
|
||||
{
|
||||
renderScrollbars = true,
|
||||
renderSelection = true,
|
||||
// Whether to employ render optimizations to improve performance.
|
||||
// Should not be turned on for export operations and similar, because it
|
||||
// doesn't guarantee pixel-perfect output.
|
||||
renderOptimizations = false,
|
||||
renderGrid = true,
|
||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||
CSS filters) */
|
||||
isExport = false,
|
||||
}: {
|
||||
renderScrollbars?: boolean;
|
||||
renderSelection?: boolean;
|
||||
renderOptimizations?: boolean;
|
||||
renderGrid?: boolean;
|
||||
isExport?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
if (canvas === null) {
|
||||
return { atLeastOneVisibleElement: false };
|
||||
}
|
||||
|
||||
const {
|
||||
renderScrollbars = true,
|
||||
renderSelection = true,
|
||||
renderGrid = true,
|
||||
isExporting,
|
||||
} = renderConfig;
|
||||
|
||||
const context = canvas.getContext("2d")!;
|
||||
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
@ -215,22 +204,22 @@ export const renderScene = (
|
||||
const normalizedCanvasWidth = canvas.width / scale;
|
||||
const normalizedCanvasHeight = canvas.height / scale;
|
||||
|
||||
if (isExport && sceneState.theme === "dark") {
|
||||
if (isExporting && renderConfig.theme === "dark") {
|
||||
context.filter = THEME_FILTER;
|
||||
}
|
||||
|
||||
// Paint background
|
||||
if (typeof sceneState.viewBackgroundColor === "string") {
|
||||
if (typeof renderConfig.viewBackgroundColor === "string") {
|
||||
const hasTransparence =
|
||||
sceneState.viewBackgroundColor === "transparent" ||
|
||||
sceneState.viewBackgroundColor.length === 5 || // #RGBA
|
||||
sceneState.viewBackgroundColor.length === 9 || // #RRGGBBA
|
||||
/(hsla|rgba)\(/.test(sceneState.viewBackgroundColor);
|
||||
renderConfig.viewBackgroundColor === "transparent" ||
|
||||
renderConfig.viewBackgroundColor.length === 5 || // #RGBA
|
||||
renderConfig.viewBackgroundColor.length === 9 || // #RRGGBBA
|
||||
/(hsla|rgba)\(/.test(renderConfig.viewBackgroundColor);
|
||||
if (hasTransparence) {
|
||||
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||
}
|
||||
context.save();
|
||||
context.fillStyle = sceneState.viewBackgroundColor;
|
||||
context.fillStyle = renderConfig.viewBackgroundColor;
|
||||
context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||
context.restore();
|
||||
} else {
|
||||
@ -238,42 +227,46 @@ export const renderScene = (
|
||||
}
|
||||
|
||||
// Apply zoom
|
||||
const zoomTranslationX = sceneState.zoom.translation.x;
|
||||
const zoomTranslationY = sceneState.zoom.translation.y;
|
||||
const zoomTranslationX = renderConfig.zoom.translation.x;
|
||||
const zoomTranslationY = renderConfig.zoom.translation.y;
|
||||
context.save();
|
||||
context.translate(zoomTranslationX, zoomTranslationY);
|
||||
context.scale(sceneState.zoom.value, sceneState.zoom.value);
|
||||
context.scale(renderConfig.zoom.value, renderConfig.zoom.value);
|
||||
|
||||
// Grid
|
||||
if (renderGrid && appState.gridSize) {
|
||||
strokeGrid(
|
||||
context,
|
||||
appState.gridSize,
|
||||
-Math.ceil(zoomTranslationX / sceneState.zoom.value / appState.gridSize) *
|
||||
-Math.ceil(
|
||||
zoomTranslationX / renderConfig.zoom.value / appState.gridSize,
|
||||
) *
|
||||
appState.gridSize +
|
||||
(sceneState.scrollX % appState.gridSize),
|
||||
-Math.ceil(zoomTranslationY / sceneState.zoom.value / appState.gridSize) *
|
||||
(renderConfig.scrollX % appState.gridSize),
|
||||
-Math.ceil(
|
||||
zoomTranslationY / renderConfig.zoom.value / appState.gridSize,
|
||||
) *
|
||||
appState.gridSize +
|
||||
(sceneState.scrollY % appState.gridSize),
|
||||
normalizedCanvasWidth / sceneState.zoom.value,
|
||||
normalizedCanvasHeight / sceneState.zoom.value,
|
||||
(renderConfig.scrollY % appState.gridSize),
|
||||
normalizedCanvasWidth / renderConfig.zoom.value,
|
||||
normalizedCanvasHeight / renderConfig.zoom.value,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint visible elements
|
||||
const visibleElements = elements.filter((element) =>
|
||||
isVisibleElement(element, normalizedCanvasWidth, normalizedCanvasHeight, {
|
||||
zoom: sceneState.zoom,
|
||||
zoom: renderConfig.zoom,
|
||||
offsetLeft: appState.offsetLeft,
|
||||
offsetTop: appState.offsetTop,
|
||||
scrollX: sceneState.scrollX,
|
||||
scrollY: sceneState.scrollY,
|
||||
scrollX: renderConfig.scrollX,
|
||||
scrollY: renderConfig.scrollY,
|
||||
}),
|
||||
);
|
||||
|
||||
visibleElements.forEach((element) => {
|
||||
try {
|
||||
renderElement(element, rc, context, renderOptimizations, sceneState);
|
||||
renderElement(element, rc, context, renderConfig);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -284,20 +277,14 @@ export const renderScene = (
|
||||
appState.editingLinearElement.elementId,
|
||||
);
|
||||
if (element) {
|
||||
renderLinearPointHandles(context, appState, sceneState, element);
|
||||
renderLinearPointHandles(context, appState, renderConfig, element);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint selection element
|
||||
if (selectionElement) {
|
||||
try {
|
||||
renderElement(
|
||||
selectionElement,
|
||||
rc,
|
||||
context,
|
||||
renderOptimizations,
|
||||
sceneState,
|
||||
);
|
||||
renderElement(selectionElement, rc, context, renderConfig);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -307,7 +294,7 @@ export const renderScene = (
|
||||
appState.suggestedBindings
|
||||
.filter((binding) => binding != null)
|
||||
.forEach((suggestedBinding) => {
|
||||
renderBindingHighlight(context, sceneState, suggestedBinding!);
|
||||
renderBindingHighlight(context, renderConfig, suggestedBinding!);
|
||||
});
|
||||
}
|
||||
|
||||
@ -327,12 +314,14 @@ export const renderScene = (
|
||||
selectionColors.push(oc.black);
|
||||
}
|
||||
// remote users
|
||||
if (sceneState.remoteSelectedElementIds[element.id]) {
|
||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||
selectionColors.push(
|
||||
...sceneState.remoteSelectedElementIds[element.id].map((socketId) => {
|
||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||
(socketId) => {
|
||||
const { background } = getClientColors(socketId, appState);
|
||||
return background;
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (selectionColors.length) {
|
||||
@ -374,37 +363,37 @@ export const renderScene = (
|
||||
}
|
||||
|
||||
selections.forEach((selection) =>
|
||||
renderSelectionBorder(context, sceneState, selection),
|
||||
renderSelectionBorder(context, renderConfig, selection),
|
||||
);
|
||||
|
||||
const locallySelectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
// Paint resize transformHandles
|
||||
context.save();
|
||||
context.translate(sceneState.scrollX, sceneState.scrollY);
|
||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||
if (locallySelectedElements.length === 1) {
|
||||
context.fillStyle = oc.white;
|
||||
const transformHandles = getTransformHandles(
|
||||
locallySelectedElements[0],
|
||||
sceneState.zoom,
|
||||
renderConfig.zoom,
|
||||
"mouse", // when we render we don't know which pointer type so use mouse
|
||||
);
|
||||
if (!appState.viewModeEnabled) {
|
||||
renderTransformHandles(
|
||||
context,
|
||||
sceneState,
|
||||
renderConfig,
|
||||
transformHandles,
|
||||
locallySelectedElements[0].angle,
|
||||
);
|
||||
}
|
||||
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
||||
const dashedLinePadding = 4 / sceneState.zoom.value;
|
||||
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
||||
context.fillStyle = oc.white;
|
||||
const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
|
||||
const initialLineDash = context.getLineDash();
|
||||
context.setLineDash([2 / sceneState.zoom.value]);
|
||||
context.setLineDash([2 / renderConfig.zoom.value]);
|
||||
const lineWidth = context.lineWidth;
|
||||
context.lineWidth = 1 / sceneState.zoom.value;
|
||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
||||
strokeRectWithRotation(
|
||||
context,
|
||||
x1 - dashedLinePadding,
|
||||
@ -420,11 +409,11 @@ export const renderScene = (
|
||||
const transformHandles = getTransformHandlesFromCoords(
|
||||
[x1, y1, x2, y2],
|
||||
0,
|
||||
sceneState.zoom,
|
||||
renderConfig.zoom,
|
||||
"mouse",
|
||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||
);
|
||||
renderTransformHandles(context, sceneState, transformHandles, 0);
|
||||
renderTransformHandles(context, renderConfig, transformHandles, 0);
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
@ -433,8 +422,8 @@ export const renderScene = (
|
||||
context.restore();
|
||||
|
||||
// Paint remote pointers
|
||||
for (const clientId in sceneState.remotePointerViewportCoords) {
|
||||
let { x, y } = sceneState.remotePointerViewportCoords[clientId];
|
||||
for (const clientId in renderConfig.remotePointerViewportCoords) {
|
||||
let { x, y } = renderConfig.remotePointerViewportCoords[clientId];
|
||||
|
||||
x -= appState.offsetLeft;
|
||||
y -= appState.offsetTop;
|
||||
@ -459,14 +448,14 @@ export const renderScene = (
|
||||
context.strokeStyle = stroke;
|
||||
context.fillStyle = background;
|
||||
|
||||
const userState = sceneState.remotePointerUserStates[clientId];
|
||||
const userState = renderConfig.remotePointerUserStates[clientId];
|
||||
if (isOutOfBounds || userState === UserIdleState.AWAY) {
|
||||
context.globalAlpha = 0.48;
|
||||
}
|
||||
|
||||
if (
|
||||
sceneState.remotePointerButton &&
|
||||
sceneState.remotePointerButton[clientId] === "down"
|
||||
renderConfig.remotePointerButton &&
|
||||
renderConfig.remotePointerButton[clientId] === "down"
|
||||
) {
|
||||
context.beginPath();
|
||||
context.arc(x, y, 15, 0, 2 * Math.PI, false);
|
||||
@ -492,7 +481,7 @@ export const renderScene = (
|
||||
context.fill();
|
||||
context.stroke();
|
||||
|
||||
const username = sceneState.remotePointerUsernames[clientId];
|
||||
const username = renderConfig.remotePointerUsernames[clientId];
|
||||
|
||||
let idleState = "";
|
||||
if (userState === UserIdleState.AWAY) {
|
||||
@ -552,7 +541,7 @@ export const renderScene = (
|
||||
elements,
|
||||
normalizedCanvasWidth,
|
||||
normalizedCanvasHeight,
|
||||
sceneState,
|
||||
renderConfig,
|
||||
);
|
||||
|
||||
context.save();
|
||||
@ -579,7 +568,7 @@ export const renderScene = (
|
||||
|
||||
const renderTransformHandles = (
|
||||
context: CanvasRenderingContext2D,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
transformHandles: TransformHandles,
|
||||
angle: number,
|
||||
): void => {
|
||||
@ -587,7 +576,7 @@ const renderTransformHandles = (
|
||||
const transformHandle = transformHandles[key as TransformHandleType];
|
||||
if (transformHandle !== undefined) {
|
||||
context.save();
|
||||
context.lineWidth = 1 / sceneState.zoom.value;
|
||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
||||
if (key === "rotation") {
|
||||
fillCircle(
|
||||
context,
|
||||
@ -615,7 +604,7 @@ const renderTransformHandles = (
|
||||
|
||||
const renderSelectionBorder = (
|
||||
context: CanvasRenderingContext2D,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
elementProperties: {
|
||||
angle: number;
|
||||
elementX1: number;
|
||||
@ -630,13 +619,13 @@ const renderSelectionBorder = (
|
||||
const elementWidth = elementX2 - elementX1;
|
||||
const elementHeight = elementY2 - elementY1;
|
||||
|
||||
const dashedLinePadding = 4 / sceneState.zoom.value;
|
||||
const dashWidth = 8 / sceneState.zoom.value;
|
||||
const spaceWidth = 4 / sceneState.zoom.value;
|
||||
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
||||
const dashWidth = 8 / renderConfig.zoom.value;
|
||||
const spaceWidth = 4 / renderConfig.zoom.value;
|
||||
|
||||
context.save();
|
||||
context.translate(sceneState.scrollX, sceneState.scrollY);
|
||||
context.lineWidth = 1 / sceneState.zoom.value;
|
||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
||||
|
||||
const count = selectionColors.length;
|
||||
for (let index = 0; index < count; ++index) {
|
||||
@ -662,7 +651,7 @@ const renderSelectionBorder = (
|
||||
|
||||
const renderBindingHighlight = (
|
||||
context: CanvasRenderingContext2D,
|
||||
sceneState: SceneState,
|
||||
renderConfig: RenderConfig,
|
||||
suggestedBinding: SuggestedBinding,
|
||||
) => {
|
||||
const renderHighlight = Array.isArray(suggestedBinding)
|
||||
@ -670,7 +659,7 @@ const renderBindingHighlight = (
|
||||
: renderBindingHighlightForBindableElement;
|
||||
|
||||
context.save();
|
||||
context.translate(sceneState.scrollX, sceneState.scrollY);
|
||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||
renderHighlight(context, suggestedBinding as any);
|
||||
|
||||
context.restore();
|
||||
|
@ -51,14 +51,7 @@ export const exportToCanvas = async (
|
||||
files,
|
||||
});
|
||||
|
||||
renderScene(
|
||||
elements,
|
||||
appState,
|
||||
null,
|
||||
scale,
|
||||
rough.canvas(canvas),
|
||||
canvas,
|
||||
{
|
||||
renderScene(elements, appState, null, scale, rough.canvas(canvas), canvas, {
|
||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||
scrollX: -minX + exportPadding,
|
||||
scrollY: -minY + exportPadding,
|
||||
@ -70,15 +63,11 @@ export const exportToCanvas = async (
|
||||
remotePointerUserStates: {},
|
||||
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||
imageCache,
|
||||
},
|
||||
{
|
||||
renderScrollbars: false,
|
||||
renderSelection: false,
|
||||
renderOptimizations: true,
|
||||
renderGrid: false,
|
||||
isExport: true,
|
||||
},
|
||||
);
|
||||
isExporting: true,
|
||||
});
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
@ -1,20 +1,32 @@
|
||||
import { ExcalidrawTextElement } from "../element/types";
|
||||
import { AppClassProperties, AppState, Zoom } from "../types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
|
||||
export type SceneState = {
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
// null indicates transparent bg
|
||||
viewBackgroundColor: string | null;
|
||||
zoom: Zoom;
|
||||
shouldCacheIgnoreZoom: boolean;
|
||||
export type RenderConfig = {
|
||||
// AppState values
|
||||
// ---------------------------------------------------------------------------
|
||||
scrollX: AppState["scrollX"];
|
||||
scrollY: AppState["scrollY"];
|
||||
/** null indicates transparent bg */
|
||||
viewBackgroundColor: AppState["viewBackgroundColor"] | null;
|
||||
zoom: AppState["zoom"];
|
||||
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
||||
theme: AppState["theme"];
|
||||
// collab-related state
|
||||
// ---------------------------------------------------------------------------
|
||||
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
||||
remotePointerButton?: { [id: string]: string | undefined };
|
||||
remoteSelectedElementIds: { [elementId: string]: string[] };
|
||||
remotePointerUsernames: { [id: string]: string };
|
||||
remotePointerUserStates: { [id: string]: string };
|
||||
theme: AppState["theme"];
|
||||
// extra options passed to the renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
imageCache: AppClassProperties["imageCache"];
|
||||
renderScrollbars?: boolean;
|
||||
renderSelection?: boolean;
|
||||
renderGrid?: boolean;
|
||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||
CSS filters), and we disable render optimizations for best output */
|
||||
isExporting: boolean;
|
||||
};
|
||||
|
||||
export type SceneScroll = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user