fix: export scale quality regression (#4316)

This commit is contained in:
David Luzar 2021-11-25 14:05:22 +01:00 committed by GitHub
parent f9d2d537a2
commit 8ff159e76e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 172 deletions

View File

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

View File

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

View File

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

View File

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

View File

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