mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: export scale quality regression (#4316)
This commit is contained in:
parent
f9d2d537a2
commit
8ff159e76e
5 changed files with 179 additions and 172 deletions
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue