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