From 8ff159e76e37127868a7aa964739874e3ea36a18 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Thu, 25 Nov 2021 14:05:22 +0100 Subject: [PATCH] fix: export scale quality regression (#4316) --- src/components/App.tsx | 12 +-- src/renderer/renderElement.ts | 107 +++++++++++++---------- src/renderer/renderScene.ts | 157 ++++++++++++++++------------------ src/scene/export.ts | 45 ++++------ src/scene/types.ts | 30 +++++-- 5 files changed, 179 insertions(+), 172 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index f017593e..90b96601 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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 { 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 { shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom, theme: this.state.theme, imageCache: this.imageCache, - }, - { - renderOptimizations: true, + isExporting: false, renderScrollbars: !this.isMobile, }, ); diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 063b266a..6fa54316 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -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; } diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index a9cfa60f..9fddadd7 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -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, ) => { 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) => { - const { background } = getClientColors(socketId, appState); - return background; - }), + ...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(); diff --git a/src/scene/export.ts b/src/scene/export.ts index 086f252c..1f46bd9f 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -51,34 +51,23 @@ export const exportToCanvas = async ( files, }); - renderScene( - elements, - appState, - null, - scale, - rough.canvas(canvas), - canvas, - { - viewBackgroundColor: exportBackground ? viewBackgroundColor : null, - scrollX: -minX + exportPadding, - scrollY: -minY + exportPadding, - zoom: defaultAppState.zoom, - remotePointerViewportCoords: {}, - remoteSelectedElementIds: {}, - shouldCacheIgnoreZoom: false, - remotePointerUsernames: {}, - remotePointerUserStates: {}, - theme: appState.exportWithDarkMode ? "dark" : "light", - imageCache, - }, - { - renderScrollbars: false, - renderSelection: false, - renderOptimizations: true, - renderGrid: false, - isExport: true, - }, - ); + renderScene(elements, appState, null, scale, rough.canvas(canvas), canvas, { + viewBackgroundColor: exportBackground ? viewBackgroundColor : null, + scrollX: -minX + exportPadding, + scrollY: -minY + exportPadding, + zoom: defaultAppState.zoom, + remotePointerViewportCoords: {}, + remoteSelectedElementIds: {}, + shouldCacheIgnoreZoom: false, + remotePointerUsernames: {}, + remotePointerUserStates: {}, + theme: appState.exportWithDarkMode ? "dark" : "light", + imageCache, + renderScrollbars: false, + renderSelection: false, + renderGrid: false, + isExporting: true, + }); return canvas; }; diff --git a/src/scene/types.ts b/src/scene/types.ts index bd1d20c6..be05ddfd 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -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 = {