mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-26 16:34:22 +01:00 
			
		
		
		
	fix: export scale quality regression (#4316)
This commit is contained in:
		| @@ -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; | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 David Luzar
					David Luzar