refactor: cleanup renderScene (#5573)
* refactor: cleanup renderScene * pass object instead of individual params
This commit is contained in:
parent
c37977af4b
commit
fd946adbae
@ -1165,7 +1165,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.renderScene();
|
||||||
|
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
||||||
|
|
||||||
|
// Do not notify consumers if we're still loading the scene. Among other
|
||||||
|
// potential issues, this fixes a case where the tab isn't focused during
|
||||||
|
// init, which would trigger onChange with empty elements, which would then
|
||||||
|
// override whatever is in localStorage currently.
|
||||||
|
if (!this.state.isLoading) {
|
||||||
|
this.props.onChange?.(
|
||||||
|
this.scene.getElementsIncludingDeleted(),
|
||||||
|
this.state,
|
||||||
|
this.files,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderScene = () => {
|
||||||
const cursorButton: {
|
const cursorButton: {
|
||||||
[id: string]: string | undefined;
|
[id: string]: string | undefined;
|
||||||
} = {};
|
} = {};
|
||||||
@ -1202,6 +1218,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
cursorButton[socketId] = user.button;
|
cursorButton[socketId] = user.button;
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderingElements = this.scene
|
const renderingElements = this.scene
|
||||||
.getNonDeletedElements()
|
.getNonDeletedElements()
|
||||||
.filter((element) => {
|
.filter((element) => {
|
||||||
@ -1223,42 +1240,43 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderScene(
|
renderScene(
|
||||||
renderingElements,
|
|
||||||
this.state,
|
|
||||||
this.state.selectionElement,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
this.rc!,
|
|
||||||
this.canvas!,
|
|
||||||
{
|
{
|
||||||
scrollX: this.state.scrollX,
|
elements: renderingElements,
|
||||||
scrollY: this.state.scrollY,
|
appState: this.state,
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor,
|
scale: window.devicePixelRatio,
|
||||||
zoom: this.state.zoom,
|
rc: this.rc!,
|
||||||
remotePointerViewportCoords: pointerViewportCoords,
|
canvas: this.canvas!,
|
||||||
remotePointerButton: cursorButton,
|
renderConfig: {
|
||||||
remoteSelectedElementIds,
|
scrollX: this.state.scrollX,
|
||||||
remotePointerUsernames: pointerUsernames,
|
scrollY: this.state.scrollY,
|
||||||
remotePointerUserStates: pointerUserStates,
|
viewBackgroundColor: this.state.viewBackgroundColor,
|
||||||
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
zoom: this.state.zoom,
|
||||||
theme: this.state.theme,
|
remotePointerViewportCoords: pointerViewportCoords,
|
||||||
imageCache: this.imageCache,
|
remotePointerButton: cursorButton,
|
||||||
isExporting: false,
|
remoteSelectedElementIds,
|
||||||
renderScrollbars: !this.device.isMobile,
|
remotePointerUsernames: pointerUsernames,
|
||||||
},
|
remotePointerUserStates: pointerUserStates,
|
||||||
({ atLeastOneVisibleElement, scrollBars }) => {
|
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
||||||
if (scrollBars) {
|
theme: this.state.theme,
|
||||||
currentScrollBars = scrollBars;
|
imageCache: this.imageCache,
|
||||||
}
|
isExporting: false,
|
||||||
const scrolledOutside =
|
renderScrollbars: !this.device.isMobile,
|
||||||
// hide when editing text
|
},
|
||||||
isTextElement(this.state.editingElement)
|
callback: ({ atLeastOneVisibleElement, scrollBars }) => {
|
||||||
? false
|
if (scrollBars) {
|
||||||
: !atLeastOneVisibleElement && renderingElements.length > 0;
|
currentScrollBars = scrollBars;
|
||||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
}
|
||||||
this.setState({ scrolledOutside });
|
const scrolledOutside =
|
||||||
}
|
// hide when editing text
|
||||||
|
isTextElement(this.state.editingElement)
|
||||||
|
? false
|
||||||
|
: !atLeastOneVisibleElement && renderingElements.length > 0;
|
||||||
|
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||||
|
this.setState({ scrolledOutside });
|
||||||
|
}
|
||||||
|
|
||||||
this.scheduleImageRefresh();
|
this.scheduleImageRefresh();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
|
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
|
||||||
);
|
);
|
||||||
@ -1266,21 +1284,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (!THROTTLE_NEXT_RENDER) {
|
if (!THROTTLE_NEXT_RENDER) {
|
||||||
THROTTLE_NEXT_RENDER = true;
|
THROTTLE_NEXT_RENDER = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
|
||||||
|
|
||||||
// Do not notify consumers if we're still loading the scene. Among other
|
|
||||||
// potential issues, this fixes a case where the tab isn't focused during
|
|
||||||
// init, which would trigger onChange with empty elements, which would then
|
|
||||||
// override whatever is in localStorage currently.
|
|
||||||
if (!this.state.isLoading) {
|
|
||||||
this.props.onChange?.(
|
|
||||||
this.scene.getElementsIncludingDeleted(),
|
|
||||||
this.state,
|
|
||||||
this.files,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onScroll = debounce(() => {
|
private onScroll = debounce(() => {
|
||||||
const { offsetTop, offsetLeft } = this.getCanvasOffsets();
|
const { offsetTop, offsetLeft } = this.getCanvasOffsets();
|
||||||
|
@ -284,492 +284,475 @@ const renderLinearElementPointHighlight = (
|
|||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const _renderScene = (
|
export const _renderScene = ({
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements,
|
||||||
appState: AppState,
|
appState,
|
||||||
selectionElement: NonDeletedExcalidrawElement | null,
|
scale,
|
||||||
scale: number,
|
rc,
|
||||||
rc: RoughCanvas,
|
canvas,
|
||||||
canvas: HTMLCanvasElement,
|
renderConfig,
|
||||||
renderConfig: RenderConfig,
|
}: {
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
appState: AppState;
|
||||||
|
scale: number;
|
||||||
|
rc: RoughCanvas;
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
renderConfig: RenderConfig;
|
||||||
|
}) =>
|
||||||
// extra options passed to the renderer
|
// extra options passed to the renderer
|
||||||
) => {
|
{
|
||||||
if (canvas === null) {
|
if (canvas === null) {
|
||||||
return { atLeastOneVisibleElement: false };
|
return { atLeastOneVisibleElement: false };
|
||||||
}
|
}
|
||||||
|
const {
|
||||||
|
renderScrollbars = true,
|
||||||
|
renderSelection = true,
|
||||||
|
renderGrid = true,
|
||||||
|
isExporting,
|
||||||
|
} = renderConfig;
|
||||||
|
|
||||||
const {
|
const context = canvas.getContext("2d")!;
|
||||||
renderScrollbars = true,
|
|
||||||
renderSelection = true,
|
|
||||||
renderGrid = true,
|
|
||||||
isExporting,
|
|
||||||
} = renderConfig;
|
|
||||||
|
|
||||||
const context = canvas.getContext("2d")!;
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
context.save();
|
||||||
|
context.scale(scale, scale);
|
||||||
|
|
||||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
// When doing calculations based on canvas width we should used normalized one
|
||||||
context.save();
|
const normalizedCanvasWidth = canvas.width / scale;
|
||||||
context.scale(scale, scale);
|
const normalizedCanvasHeight = canvas.height / scale;
|
||||||
|
|
||||||
// When doing calculations based on canvas width we should used normalized one
|
if (isExporting && renderConfig.theme === "dark") {
|
||||||
const normalizedCanvasWidth = canvas.width / scale;
|
context.filter = THEME_FILTER;
|
||||||
const normalizedCanvasHeight = canvas.height / scale;
|
}
|
||||||
|
|
||||||
if (isExporting && renderConfig.theme === "dark") {
|
// Paint background
|
||||||
context.filter = THEME_FILTER;
|
if (typeof renderConfig.viewBackgroundColor === "string") {
|
||||||
}
|
const hasTransparence =
|
||||||
|
renderConfig.viewBackgroundColor === "transparent" ||
|
||||||
// Paint background
|
renderConfig.viewBackgroundColor.length === 5 || // #RGBA
|
||||||
if (typeof renderConfig.viewBackgroundColor === "string") {
|
renderConfig.viewBackgroundColor.length === 9 || // #RRGGBBA
|
||||||
const hasTransparence =
|
/(hsla|rgba)\(/.test(renderConfig.viewBackgroundColor);
|
||||||
renderConfig.viewBackgroundColor === "transparent" ||
|
if (hasTransparence) {
|
||||||
renderConfig.viewBackgroundColor.length === 5 || // #RGBA
|
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||||
renderConfig.viewBackgroundColor.length === 9 || // #RRGGBBA
|
}
|
||||||
/(hsla|rgba)\(/.test(renderConfig.viewBackgroundColor);
|
context.save();
|
||||||
if (hasTransparence) {
|
context.fillStyle = renderConfig.viewBackgroundColor;
|
||||||
|
context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||||
|
context.restore();
|
||||||
|
} else {
|
||||||
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply zoom
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle = renderConfig.viewBackgroundColor;
|
context.scale(renderConfig.zoom.value, renderConfig.zoom.value);
|
||||||
context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
|
||||||
context.restore();
|
|
||||||
} else {
|
|
||||||
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply zoom
|
// Grid
|
||||||
context.save();
|
if (renderGrid && appState.gridSize) {
|
||||||
context.scale(renderConfig.zoom.value, renderConfig.zoom.value);
|
strokeGrid(
|
||||||
|
|
||||||
// Grid
|
|
||||||
if (renderGrid && appState.gridSize) {
|
|
||||||
strokeGrid(
|
|
||||||
context,
|
|
||||||
appState.gridSize,
|
|
||||||
-Math.ceil(renderConfig.zoom.value / appState.gridSize) *
|
|
||||||
appState.gridSize +
|
|
||||||
(renderConfig.scrollX % appState.gridSize),
|
|
||||||
-Math.ceil(renderConfig.zoom.value / appState.gridSize) *
|
|
||||||
appState.gridSize +
|
|
||||||
(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: renderConfig.zoom,
|
|
||||||
offsetLeft: appState.offsetLeft,
|
|
||||||
offsetTop: appState.offsetTop,
|
|
||||||
scrollX: renderConfig.scrollX,
|
|
||||||
scrollY: renderConfig.scrollY,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
visibleElements.forEach((element) => {
|
|
||||||
try {
|
|
||||||
renderElement(element, rc, context, renderConfig);
|
|
||||||
if (!isExporting) {
|
|
||||||
renderLinkIcon(element, context, appState);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (appState.editingLinearElement) {
|
|
||||||
const element = LinearElementEditor.getElement(
|
|
||||||
appState.editingLinearElement.elementId,
|
|
||||||
);
|
|
||||||
if (element) {
|
|
||||||
renderLinearPointHandles(context, appState, renderConfig, element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint selection element
|
|
||||||
if (selectionElement) {
|
|
||||||
try {
|
|
||||||
renderElement(selectionElement, rc, context, renderConfig);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBindingEnabled(appState)) {
|
|
||||||
appState.suggestedBindings
|
|
||||||
.filter((binding) => binding != null)
|
|
||||||
.forEach((suggestedBinding) => {
|
|
||||||
renderBindingHighlight(context, renderConfig, suggestedBinding!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
appState.selectedLinearElement &&
|
|
||||||
appState.selectedLinearElement.hoverPointIndex >= 0
|
|
||||||
) {
|
|
||||||
renderLinearElementPointHighlight(context, appState, renderConfig);
|
|
||||||
}
|
|
||||||
// Paint selected elements
|
|
||||||
if (
|
|
||||||
renderSelection &&
|
|
||||||
!appState.multiElement &&
|
|
||||||
!appState.editingLinearElement
|
|
||||||
) {
|
|
||||||
const locallySelectedElements = getSelectedElements(elements, appState);
|
|
||||||
const showBoundingBox = shouldShowBoundingBox(locallySelectedElements);
|
|
||||||
|
|
||||||
const locallySelectedIds = locallySelectedElements.map(
|
|
||||||
(element) => element.id,
|
|
||||||
);
|
|
||||||
const isSingleLinearElementSelected =
|
|
||||||
locallySelectedElements.length === 1 &&
|
|
||||||
isLinearElement(locallySelectedElements[0]);
|
|
||||||
// render selected linear element points
|
|
||||||
if (
|
|
||||||
isSingleLinearElementSelected &&
|
|
||||||
appState.selectedLinearElement?.elementId ===
|
|
||||||
locallySelectedElements[0].id &&
|
|
||||||
!locallySelectedElements[0].locked
|
|
||||||
) {
|
|
||||||
renderLinearPointHandles(
|
|
||||||
context,
|
context,
|
||||||
appState,
|
appState.gridSize,
|
||||||
renderConfig,
|
-Math.ceil(renderConfig.zoom.value / appState.gridSize) *
|
||||||
locallySelectedElements[0] as ExcalidrawLinearElement,
|
appState.gridSize +
|
||||||
);
|
(renderConfig.scrollX % appState.gridSize),
|
||||||
}
|
-Math.ceil(renderConfig.zoom.value / appState.gridSize) *
|
||||||
if (showBoundingBox) {
|
appState.gridSize +
|
||||||
const selections = elements.reduce((acc, element) => {
|
(renderConfig.scrollY % appState.gridSize),
|
||||||
const selectionColors = [];
|
normalizedCanvasWidth / renderConfig.zoom.value,
|
||||||
// local user
|
normalizedCanvasHeight / renderConfig.zoom.value,
|
||||||
if (
|
|
||||||
locallySelectedIds.includes(element.id) &&
|
|
||||||
!isSelectedViaGroup(appState, element)
|
|
||||||
) {
|
|
||||||
selectionColors.push(oc.black);
|
|
||||||
}
|
|
||||||
// remote users
|
|
||||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
|
||||||
selectionColors.push(
|
|
||||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
|
||||||
(socketId) => {
|
|
||||||
const { background } = getClientColors(socketId, appState);
|
|
||||||
return background;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (selectionColors.length) {
|
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
|
||||||
getElementAbsoluteCoords(element);
|
|
||||||
acc.push({
|
|
||||||
angle: element.angle,
|
|
||||||
elementX1,
|
|
||||||
elementY1,
|
|
||||||
elementX2,
|
|
||||||
elementY2,
|
|
||||||
selectionColors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as { angle: number; elementX1: number; elementY1: number; elementX2: number; elementY2: number; selectionColors: string[] }[]);
|
|
||||||
|
|
||||||
const addSelectionForGroupId = (groupId: GroupId) => {
|
|
||||||
const groupElements = getElementsInGroup(elements, groupId);
|
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
|
||||||
getCommonBounds(groupElements);
|
|
||||||
selections.push({
|
|
||||||
angle: 0,
|
|
||||||
elementX1,
|
|
||||||
elementX2,
|
|
||||||
elementY1,
|
|
||||||
elementY2,
|
|
||||||
selectionColors: [oc.black],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const groupId of getSelectedGroupIds(appState)) {
|
|
||||||
// TODO: support multiplayer selected group IDs
|
|
||||||
addSelectionForGroupId(groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appState.editingGroupId) {
|
|
||||||
addSelectionForGroupId(appState.editingGroupId);
|
|
||||||
}
|
|
||||||
selections.forEach((selection) =>
|
|
||||||
renderSelectionBorder(
|
|
||||||
context,
|
|
||||||
renderConfig,
|
|
||||||
selection,
|
|
||||||
isSingleLinearElementSelected ? DEFAULT_SPACING * 2 : DEFAULT_SPACING,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Paint resize transformHandles
|
|
||||||
context.save();
|
|
||||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
|
||||||
|
|
||||||
if (locallySelectedElements.length === 1) {
|
|
||||||
context.fillStyle = oc.white;
|
|
||||||
const transformHandles = getTransformHandles(
|
|
||||||
locallySelectedElements[0],
|
|
||||||
renderConfig.zoom,
|
|
||||||
"mouse", // when we render we don't know which pointer type so use mouse
|
|
||||||
);
|
|
||||||
if (!appState.viewModeEnabled && showBoundingBox) {
|
|
||||||
renderTransformHandles(
|
|
||||||
context,
|
|
||||||
renderConfig,
|
|
||||||
transformHandles,
|
|
||||||
locallySelectedElements[0].angle,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
|
||||||
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
|
||||||
context.fillStyle = oc.white;
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
|
|
||||||
const initialLineDash = context.getLineDash();
|
|
||||||
context.setLineDash([2 / renderConfig.zoom.value]);
|
|
||||||
const lineWidth = context.lineWidth;
|
|
||||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
|
||||||
strokeRectWithRotation(
|
|
||||||
context,
|
|
||||||
x1 - dashedLinePadding,
|
|
||||||
y1 - dashedLinePadding,
|
|
||||||
x2 - x1 + dashedLinePadding * 2,
|
|
||||||
y2 - y1 + dashedLinePadding * 2,
|
|
||||||
(x1 + x2) / 2,
|
|
||||||
(y1 + y2) / 2,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
context.lineWidth = lineWidth;
|
|
||||||
context.setLineDash(initialLineDash);
|
|
||||||
const transformHandles = getTransformHandlesFromCoords(
|
|
||||||
[x1, y1, x2, y2],
|
|
||||||
0,
|
|
||||||
renderConfig.zoom,
|
|
||||||
"mouse",
|
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
|
||||||
);
|
|
||||||
if (locallySelectedElements.some((element) => !element.locked)) {
|
|
||||||
renderTransformHandles(context, renderConfig, transformHandles, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset zoom
|
|
||||||
context.restore();
|
|
||||||
|
|
||||||
// Paint remote pointers
|
|
||||||
for (const clientId in renderConfig.remotePointerViewportCoords) {
|
|
||||||
let { x, y } = renderConfig.remotePointerViewportCoords[clientId];
|
|
||||||
|
|
||||||
x -= appState.offsetLeft;
|
|
||||||
y -= appState.offsetTop;
|
|
||||||
|
|
||||||
const width = 9;
|
|
||||||
const height = 14;
|
|
||||||
|
|
||||||
const isOutOfBounds =
|
|
||||||
x < 0 ||
|
|
||||||
x > normalizedCanvasWidth - width ||
|
|
||||||
y < 0 ||
|
|
||||||
y > normalizedCanvasHeight - height;
|
|
||||||
|
|
||||||
x = Math.max(x, 0);
|
|
||||||
x = Math.min(x, normalizedCanvasWidth - width);
|
|
||||||
y = Math.max(y, 0);
|
|
||||||
y = Math.min(y, normalizedCanvasHeight - height);
|
|
||||||
|
|
||||||
const { background, stroke } = getClientColors(clientId, appState);
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
context.strokeStyle = stroke;
|
|
||||||
context.fillStyle = background;
|
|
||||||
|
|
||||||
const userState = renderConfig.remotePointerUserStates[clientId];
|
|
||||||
if (isOutOfBounds || userState === UserIdleState.AWAY) {
|
|
||||||
context.globalAlpha = 0.48;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
renderConfig.remotePointerButton &&
|
|
||||||
renderConfig.remotePointerButton[clientId] === "down"
|
|
||||||
) {
|
|
||||||
context.beginPath();
|
|
||||||
context.arc(x, y, 15, 0, 2 * Math.PI, false);
|
|
||||||
context.lineWidth = 3;
|
|
||||||
context.strokeStyle = "#ffffff88";
|
|
||||||
context.stroke();
|
|
||||||
context.closePath();
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.arc(x, y, 15, 0, 2 * Math.PI, false);
|
|
||||||
context.lineWidth = 1;
|
|
||||||
context.strokeStyle = stroke;
|
|
||||||
context.stroke();
|
|
||||||
context.closePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(x, y);
|
|
||||||
context.lineTo(x + 1, y + 14);
|
|
||||||
context.lineTo(x + 4, y + 9);
|
|
||||||
context.lineTo(x + 9, y + 10);
|
|
||||||
context.lineTo(x, y);
|
|
||||||
context.fill();
|
|
||||||
context.stroke();
|
|
||||||
|
|
||||||
const username = renderConfig.remotePointerUsernames[clientId];
|
|
||||||
|
|
||||||
let idleState = "";
|
|
||||||
if (userState === UserIdleState.AWAY) {
|
|
||||||
idleState = hasEmojiSupport ? "⚫️" : ` (${UserIdleState.AWAY})`;
|
|
||||||
} else if (userState === UserIdleState.IDLE) {
|
|
||||||
idleState = hasEmojiSupport ? "💤" : ` (${UserIdleState.IDLE})`;
|
|
||||||
} else if (userState === UserIdleState.ACTIVE) {
|
|
||||||
idleState = hasEmojiSupport ? "🟢" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const usernameAndIdleState = `${
|
|
||||||
username ? `${username} ` : ""
|
|
||||||
}${idleState}`;
|
|
||||||
|
|
||||||
if (!isOutOfBounds && usernameAndIdleState) {
|
|
||||||
const offsetX = x + width;
|
|
||||||
const offsetY = y + height;
|
|
||||||
const paddingHorizontal = 4;
|
|
||||||
const paddingVertical = 4;
|
|
||||||
const measure = context.measureText(usernameAndIdleState);
|
|
||||||
const measureHeight =
|
|
||||||
measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
|
|
||||||
|
|
||||||
// Border
|
|
||||||
context.fillStyle = stroke;
|
|
||||||
context.fillRect(
|
|
||||||
offsetX - 1,
|
|
||||||
offsetY - 1,
|
|
||||||
measure.width + 2 * paddingHorizontal + 2,
|
|
||||||
measureHeight + 2 * paddingVertical + 2,
|
|
||||||
);
|
|
||||||
// Background
|
|
||||||
context.fillStyle = background;
|
|
||||||
context.fillRect(
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
measure.width + 2 * paddingHorizontal,
|
|
||||||
measureHeight + 2 * paddingVertical,
|
|
||||||
);
|
|
||||||
context.fillStyle = oc.white;
|
|
||||||
|
|
||||||
context.fillText(
|
|
||||||
usernameAndIdleState,
|
|
||||||
offsetX + paddingHorizontal,
|
|
||||||
offsetY + paddingVertical + measure.actualBoundingBoxAscent,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.restore();
|
// Paint visible elements
|
||||||
context.closePath();
|
const visibleElements = elements.filter((element) =>
|
||||||
}
|
isVisibleElement(element, normalizedCanvasWidth, normalizedCanvasHeight, {
|
||||||
|
zoom: renderConfig.zoom,
|
||||||
// Paint scrollbars
|
offsetLeft: appState.offsetLeft,
|
||||||
let scrollBars;
|
offsetTop: appState.offsetTop,
|
||||||
if (renderScrollbars) {
|
scrollX: renderConfig.scrollX,
|
||||||
scrollBars = getScrollBars(
|
scrollY: renderConfig.scrollY,
|
||||||
elements,
|
}),
|
||||||
normalizedCanvasWidth,
|
|
||||||
normalizedCanvasHeight,
|
|
||||||
renderConfig,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
context.save();
|
visibleElements.forEach((element) => {
|
||||||
context.fillStyle = SCROLLBAR_COLOR;
|
try {
|
||||||
context.strokeStyle = "rgba(255,255,255,0.8)";
|
renderElement(element, rc, context, renderConfig);
|
||||||
[scrollBars.horizontal, scrollBars.vertical].forEach((scrollBar) => {
|
if (!isExporting) {
|
||||||
if (scrollBar) {
|
renderLinkIcon(element, context, appState);
|
||||||
roundRect(
|
}
|
||||||
context,
|
} catch (error: any) {
|
||||||
scrollBar.x,
|
console.error(error);
|
||||||
scrollBar.y,
|
|
||||||
scrollBar.width,
|
|
||||||
scrollBar.height,
|
|
||||||
SCROLLBAR_WIDTH / 2,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.restore();
|
if (appState.editingLinearElement) {
|
||||||
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
|
const element = LinearElementEditor.getElement(
|
||||||
};
|
appState.editingLinearElement.elementId,
|
||||||
|
);
|
||||||
|
if (element) {
|
||||||
|
renderLinearPointHandles(context, appState, renderConfig, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint selection element
|
||||||
|
if (appState.selectionElement) {
|
||||||
|
try {
|
||||||
|
renderElement(appState.selectionElement, rc, context, renderConfig);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBindingEnabled(appState)) {
|
||||||
|
appState.suggestedBindings
|
||||||
|
.filter((binding) => binding != null)
|
||||||
|
.forEach((suggestedBinding) => {
|
||||||
|
renderBindingHighlight(context, renderConfig, suggestedBinding!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState.selectedLinearElement &&
|
||||||
|
appState.selectedLinearElement.hoverPointIndex >= 0
|
||||||
|
) {
|
||||||
|
renderLinearElementPointHighlight(context, appState, renderConfig);
|
||||||
|
}
|
||||||
|
// Paint selected elements
|
||||||
|
if (
|
||||||
|
renderSelection &&
|
||||||
|
!appState.multiElement &&
|
||||||
|
!appState.editingLinearElement
|
||||||
|
) {
|
||||||
|
const locallySelectedElements = getSelectedElements(elements, appState);
|
||||||
|
const showBoundingBox = shouldShowBoundingBox(locallySelectedElements);
|
||||||
|
|
||||||
|
const locallySelectedIds = locallySelectedElements.map(
|
||||||
|
(element) => element.id,
|
||||||
|
);
|
||||||
|
const isSingleLinearElementSelected =
|
||||||
|
locallySelectedElements.length === 1 &&
|
||||||
|
isLinearElement(locallySelectedElements[0]);
|
||||||
|
// render selected linear element points
|
||||||
|
if (
|
||||||
|
isSingleLinearElementSelected &&
|
||||||
|
appState.selectedLinearElement?.elementId ===
|
||||||
|
locallySelectedElements[0].id &&
|
||||||
|
!locallySelectedElements[0].locked
|
||||||
|
) {
|
||||||
|
renderLinearPointHandles(
|
||||||
|
context,
|
||||||
|
appState,
|
||||||
|
renderConfig,
|
||||||
|
locallySelectedElements[0] as ExcalidrawLinearElement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showBoundingBox) {
|
||||||
|
const selections = elements.reduce((acc, element) => {
|
||||||
|
const selectionColors = [];
|
||||||
|
// local user
|
||||||
|
if (
|
||||||
|
locallySelectedIds.includes(element.id) &&
|
||||||
|
!isSelectedViaGroup(appState, element)
|
||||||
|
) {
|
||||||
|
selectionColors.push(oc.black);
|
||||||
|
}
|
||||||
|
// remote users
|
||||||
|
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||||
|
selectionColors.push(
|
||||||
|
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||||
|
(socketId) => {
|
||||||
|
const { background } = getClientColors(socketId, appState);
|
||||||
|
return background;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (selectionColors.length) {
|
||||||
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
|
getElementAbsoluteCoords(element);
|
||||||
|
acc.push({
|
||||||
|
angle: element.angle,
|
||||||
|
elementX1,
|
||||||
|
elementY1,
|
||||||
|
elementX2,
|
||||||
|
elementY2,
|
||||||
|
selectionColors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as { angle: number; elementX1: number; elementY1: number; elementX2: number; elementY2: number; selectionColors: string[] }[]);
|
||||||
|
|
||||||
|
const addSelectionForGroupId = (groupId: GroupId) => {
|
||||||
|
const groupElements = getElementsInGroup(elements, groupId);
|
||||||
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
|
getCommonBounds(groupElements);
|
||||||
|
selections.push({
|
||||||
|
angle: 0,
|
||||||
|
elementX1,
|
||||||
|
elementX2,
|
||||||
|
elementY1,
|
||||||
|
elementY2,
|
||||||
|
selectionColors: [oc.black],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const groupId of getSelectedGroupIds(appState)) {
|
||||||
|
// TODO: support multiplayer selected group IDs
|
||||||
|
addSelectionForGroupId(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appState.editingGroupId) {
|
||||||
|
addSelectionForGroupId(appState.editingGroupId);
|
||||||
|
}
|
||||||
|
selections.forEach((selection) =>
|
||||||
|
renderSelectionBorder(
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
selection,
|
||||||
|
isSingleLinearElementSelected
|
||||||
|
? DEFAULT_SPACING * 2
|
||||||
|
: DEFAULT_SPACING,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Paint resize transformHandles
|
||||||
|
context.save();
|
||||||
|
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||||
|
|
||||||
|
if (locallySelectedElements.length === 1) {
|
||||||
|
context.fillStyle = oc.white;
|
||||||
|
const transformHandles = getTransformHandles(
|
||||||
|
locallySelectedElements[0],
|
||||||
|
renderConfig.zoom,
|
||||||
|
"mouse", // when we render we don't know which pointer type so use mouse
|
||||||
|
);
|
||||||
|
if (!appState.viewModeEnabled && showBoundingBox) {
|
||||||
|
renderTransformHandles(
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
transformHandles,
|
||||||
|
locallySelectedElements[0].angle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
||||||
|
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
||||||
|
context.fillStyle = oc.white;
|
||||||
|
const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
|
||||||
|
const initialLineDash = context.getLineDash();
|
||||||
|
context.setLineDash([2 / renderConfig.zoom.value]);
|
||||||
|
const lineWidth = context.lineWidth;
|
||||||
|
context.lineWidth = 1 / renderConfig.zoom.value;
|
||||||
|
strokeRectWithRotation(
|
||||||
|
context,
|
||||||
|
x1 - dashedLinePadding,
|
||||||
|
y1 - dashedLinePadding,
|
||||||
|
x2 - x1 + dashedLinePadding * 2,
|
||||||
|
y2 - y1 + dashedLinePadding * 2,
|
||||||
|
(x1 + x2) / 2,
|
||||||
|
(y1 + y2) / 2,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
context.lineWidth = lineWidth;
|
||||||
|
context.setLineDash(initialLineDash);
|
||||||
|
const transformHandles = getTransformHandlesFromCoords(
|
||||||
|
[x1, y1, x2, y2],
|
||||||
|
0,
|
||||||
|
renderConfig.zoom,
|
||||||
|
"mouse",
|
||||||
|
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||||
|
);
|
||||||
|
if (locallySelectedElements.some((element) => !element.locked)) {
|
||||||
|
renderTransformHandles(context, renderConfig, transformHandles, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset zoom
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
// Paint remote pointers
|
||||||
|
for (const clientId in renderConfig.remotePointerViewportCoords) {
|
||||||
|
let { x, y } = renderConfig.remotePointerViewportCoords[clientId];
|
||||||
|
|
||||||
|
x -= appState.offsetLeft;
|
||||||
|
y -= appState.offsetTop;
|
||||||
|
|
||||||
|
const width = 9;
|
||||||
|
const height = 14;
|
||||||
|
|
||||||
|
const isOutOfBounds =
|
||||||
|
x < 0 ||
|
||||||
|
x > normalizedCanvasWidth - width ||
|
||||||
|
y < 0 ||
|
||||||
|
y > normalizedCanvasHeight - height;
|
||||||
|
|
||||||
|
x = Math.max(x, 0);
|
||||||
|
x = Math.min(x, normalizedCanvasWidth - width);
|
||||||
|
y = Math.max(y, 0);
|
||||||
|
y = Math.min(y, normalizedCanvasHeight - height);
|
||||||
|
|
||||||
|
const { background, stroke } = getClientColors(clientId, appState);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = stroke;
|
||||||
|
context.fillStyle = background;
|
||||||
|
|
||||||
|
const userState = renderConfig.remotePointerUserStates[clientId];
|
||||||
|
if (isOutOfBounds || userState === UserIdleState.AWAY) {
|
||||||
|
context.globalAlpha = 0.48;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
renderConfig.remotePointerButton &&
|
||||||
|
renderConfig.remotePointerButton[clientId] === "down"
|
||||||
|
) {
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(x, y, 15, 0, 2 * Math.PI, false);
|
||||||
|
context.lineWidth = 3;
|
||||||
|
context.strokeStyle = "#ffffff88";
|
||||||
|
context.stroke();
|
||||||
|
context.closePath();
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(x, y, 15, 0, 2 * Math.PI, false);
|
||||||
|
context.lineWidth = 1;
|
||||||
|
context.strokeStyle = stroke;
|
||||||
|
context.stroke();
|
||||||
|
context.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x, y);
|
||||||
|
context.lineTo(x + 1, y + 14);
|
||||||
|
context.lineTo(x + 4, y + 9);
|
||||||
|
context.lineTo(x + 9, y + 10);
|
||||||
|
context.lineTo(x, y);
|
||||||
|
context.fill();
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
const username = renderConfig.remotePointerUsernames[clientId];
|
||||||
|
|
||||||
|
let idleState = "";
|
||||||
|
if (userState === UserIdleState.AWAY) {
|
||||||
|
idleState = hasEmojiSupport ? "⚫️" : ` (${UserIdleState.AWAY})`;
|
||||||
|
} else if (userState === UserIdleState.IDLE) {
|
||||||
|
idleState = hasEmojiSupport ? "💤" : ` (${UserIdleState.IDLE})`;
|
||||||
|
} else if (userState === UserIdleState.ACTIVE) {
|
||||||
|
idleState = hasEmojiSupport ? "🟢" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernameAndIdleState = `${
|
||||||
|
username ? `${username} ` : ""
|
||||||
|
}${idleState}`;
|
||||||
|
|
||||||
|
if (!isOutOfBounds && usernameAndIdleState) {
|
||||||
|
const offsetX = x + width;
|
||||||
|
const offsetY = y + height;
|
||||||
|
const paddingHorizontal = 4;
|
||||||
|
const paddingVertical = 4;
|
||||||
|
const measure = context.measureText(usernameAndIdleState);
|
||||||
|
const measureHeight =
|
||||||
|
measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
|
||||||
|
|
||||||
|
// Border
|
||||||
|
context.fillStyle = stroke;
|
||||||
|
context.fillRect(
|
||||||
|
offsetX - 1,
|
||||||
|
offsetY - 1,
|
||||||
|
measure.width + 2 * paddingHorizontal + 2,
|
||||||
|
measureHeight + 2 * paddingVertical + 2,
|
||||||
|
);
|
||||||
|
// Background
|
||||||
|
context.fillStyle = background;
|
||||||
|
context.fillRect(
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
measure.width + 2 * paddingHorizontal,
|
||||||
|
measureHeight + 2 * paddingVertical,
|
||||||
|
);
|
||||||
|
context.fillStyle = oc.white;
|
||||||
|
|
||||||
|
context.fillText(
|
||||||
|
usernameAndIdleState,
|
||||||
|
offsetX + paddingHorizontal,
|
||||||
|
offsetY + paddingVertical + measure.actualBoundingBoxAscent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
context.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint scrollbars
|
||||||
|
let scrollBars;
|
||||||
|
if (renderScrollbars) {
|
||||||
|
scrollBars = getScrollBars(
|
||||||
|
elements,
|
||||||
|
normalizedCanvasWidth,
|
||||||
|
normalizedCanvasHeight,
|
||||||
|
renderConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.fillStyle = SCROLLBAR_COLOR;
|
||||||
|
context.strokeStyle = "rgba(255,255,255,0.8)";
|
||||||
|
[scrollBars.horizontal, scrollBars.vertical].forEach((scrollBar) => {
|
||||||
|
if (scrollBar) {
|
||||||
|
roundRect(
|
||||||
|
context,
|
||||||
|
scrollBar.x,
|
||||||
|
scrollBar.y,
|
||||||
|
scrollBar.width,
|
||||||
|
scrollBar.height,
|
||||||
|
SCROLLBAR_WIDTH / 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
|
||||||
|
};
|
||||||
|
|
||||||
const renderSceneThrottled = throttleRAF(
|
const renderSceneThrottled = throttleRAF(
|
||||||
(
|
(config: {
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
appState: AppState,
|
appState: AppState;
|
||||||
selectionElement: NonDeletedExcalidrawElement | null,
|
scale: number;
|
||||||
scale: number,
|
rc: RoughCanvas;
|
||||||
rc: RoughCanvas,
|
canvas: HTMLCanvasElement;
|
||||||
canvas: HTMLCanvasElement,
|
renderConfig: RenderConfig;
|
||||||
renderConfig: RenderConfig,
|
callback?: (data: ReturnType<typeof _renderScene>) => void;
|
||||||
callback?: (data: ReturnType<typeof _renderScene>) => void,
|
}) => {
|
||||||
) => {
|
const ret = _renderScene(config);
|
||||||
const ret = _renderScene(
|
config.callback?.(ret);
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
selectionElement,
|
|
||||||
scale,
|
|
||||||
rc,
|
|
||||||
canvas,
|
|
||||||
renderConfig,
|
|
||||||
);
|
|
||||||
callback?.(ret);
|
|
||||||
},
|
},
|
||||||
{ trailing: true },
|
{ trailing: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
/** renderScene throttled to animation framerate */
|
/** renderScene throttled to animation framerate */
|
||||||
export const renderScene = <T extends boolean = false>(
|
export const renderScene = <T extends boolean = false>(
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
config: {
|
||||||
appState: AppState,
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
selectionElement: NonDeletedExcalidrawElement | null,
|
appState: AppState;
|
||||||
scale: number,
|
scale: number;
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas;
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement;
|
||||||
renderConfig: RenderConfig,
|
renderConfig: RenderConfig;
|
||||||
callback?: (data: ReturnType<typeof _renderScene>) => void,
|
callback?: (data: ReturnType<typeof _renderScene>) => void;
|
||||||
|
},
|
||||||
/** Whether to throttle rendering. Defaults to false.
|
/** Whether to throttle rendering. Defaults to false.
|
||||||
* When throttling, no value is returned. Use the callback instead. */
|
* When throttling, no value is returned. Use the callback instead. */
|
||||||
throttle?: T,
|
throttle?: T,
|
||||||
): T extends true ? void : ReturnType<typeof _renderScene> => {
|
): T extends true ? void : ReturnType<typeof _renderScene> => {
|
||||||
if (throttle) {
|
if (throttle) {
|
||||||
renderSceneThrottled(
|
renderSceneThrottled(config);
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
selectionElement,
|
|
||||||
scale,
|
|
||||||
rc,
|
|
||||||
canvas,
|
|
||||||
renderConfig,
|
|
||||||
callback,
|
|
||||||
);
|
|
||||||
return undefined as T extends true ? void : ReturnType<typeof _renderScene>;
|
return undefined as T extends true ? void : ReturnType<typeof _renderScene>;
|
||||||
}
|
}
|
||||||
const ret = _renderScene(
|
const ret = _renderScene(config);
|
||||||
elements,
|
config.callback?.(ret);
|
||||||
appState,
|
|
||||||
selectionElement,
|
|
||||||
scale,
|
|
||||||
rc,
|
|
||||||
canvas,
|
|
||||||
renderConfig,
|
|
||||||
);
|
|
||||||
callback?.(ret);
|
|
||||||
return ret as T extends true ? void : ReturnType<typeof _renderScene>;
|
return ret as T extends true ? void : ReturnType<typeof _renderScene>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,22 +51,29 @@ export const exportToCanvas = async (
|
|||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
|
|
||||||
renderScene(elements, appState, null, scale, rough.canvas(canvas), canvas, {
|
renderScene({
|
||||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
elements,
|
||||||
scrollX: -minX + exportPadding,
|
appState,
|
||||||
scrollY: -minY + exportPadding,
|
scale,
|
||||||
zoom: defaultAppState.zoom,
|
rc: rough.canvas(canvas),
|
||||||
remotePointerViewportCoords: {},
|
canvas,
|
||||||
remoteSelectedElementIds: {},
|
renderConfig: {
|
||||||
shouldCacheIgnoreZoom: false,
|
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||||
remotePointerUsernames: {},
|
scrollX: -minX + exportPadding,
|
||||||
remotePointerUserStates: {},
|
scrollY: -minY + exportPadding,
|
||||||
theme: appState.exportWithDarkMode ? "dark" : "light",
|
zoom: defaultAppState.zoom,
|
||||||
imageCache,
|
remotePointerViewportCoords: {},
|
||||||
renderScrollbars: false,
|
remoteSelectedElementIds: {},
|
||||||
renderSelection: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
renderGrid: false,
|
remotePointerUsernames: {},
|
||||||
isExporting: true,
|
remotePointerUserStates: {},
|
||||||
|
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||||
|
imageCache,
|
||||||
|
renderScrollbars: false,
|
||||||
|
renderSelection: false,
|
||||||
|
renderGrid: false,
|
||||||
|
isExporting: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user