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

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