fix(editor): Arrows with text are rendered blurry in PNG export with larger scale (#11492)

This commit is contained in:
zsviczian
2026-06-22 18:22:26 +02:00
committed by GitHub
parent 28a9b1711d
commit 0642e72cfa
+38 -51
View File
@@ -889,8 +889,10 @@ export const renderElement = (
case "embeddable": { case "embeddable": {
if (renderConfig.isExporting) { if (renderConfig.isExporting) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2 + appState.scrollX; const centerX = (x1 + x2) / 2;
const cy = (y1 + y2) / 2 + appState.scrollY; const centerY = (y1 + y2) / 2;
const cx = centerX + appState.scrollX;
const cy = centerY + appState.scrollY;
let shiftX = (x2 - x1) / 2 - (element.x - x1); let shiftX = (x2 - x1) / 2 - (element.x - x1);
let shiftY = (y2 - y1) / 2 - (element.y - y1); let shiftY = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) { if (isTextElement(element)) {
@@ -912,64 +914,49 @@ export const renderElement = (
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (isArrowElement(element) && boundTextElement) { if (isArrowElement(element) && boundTextElement) {
const tempCanvas = document.createElement("canvas"); // Draw arrow directly as vector and clear label hole separately.
// This avoids temp-canvas bitmap blit which introduces resampling blur.
const tempCanvasContext = tempCanvas.getContext("2d")!;
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
const padding = getCanvasPadding(element);
tempCanvas.width =
maxDim * appState.exportScale + padding * 10 * appState.exportScale;
tempCanvas.height =
maxDim * appState.exportScale + padding * 10 * appState.exportScale;
tempCanvasContext.translate(
tempCanvas.width / 2,
tempCanvas.height / 2,
);
tempCanvasContext.scale(appState.exportScale, appState.exportScale);
// Shift the canvas to left most point of the arrow
shiftX = element.width / 2 - (element.x - x1); shiftX = element.width / 2 - (element.x - x1);
shiftY = element.height / 2 - (element.y - y1); shiftY = element.height / 2 - (element.y - y1);
tempCanvasContext.rotate(element.angle); context.save();
const tempRc = rough.canvas(tempCanvas); context.rotate(element.angle);
context.translate(-shiftX, -shiftY);
drawElementOnCanvas(element, rc, context, renderConfig);
context.restore();
tempCanvasContext.translate(-shiftX, -shiftY);
drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig);
tempCanvasContext.translate(shiftX, shiftY);
tempCanvasContext.rotate(-element.angle);
// Shift the canvas to center of bound text
const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords( const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
boundTextElement, boundTextElement,
elementsMap, elementsMap,
); );
const boundTextShiftX = (x1 + x2) / 2 - boundTextCx; const holeX =
const boundTextShiftY = (y1 + y2) / 2 - boundTextCy; boundTextCx -
tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY); centerX -
boundTextElement.width / 2 -
BOUND_TEXT_PADDING;
const holeY =
boundTextCy -
centerY -
boundTextElement.height / 2 -
BOUND_TEXT_PADDING;
const holeWidth = boundTextElement.width + BOUND_TEXT_PADDING * 2;
const holeHeight = boundTextElement.height + BOUND_TEXT_PADDING * 2;
// Clear the bound text area const isTransparentHole =
tempCanvasContext.clearRect( "viewBackgroundColor" in appState &&
-boundTextElement.width / 2, (appState.viewBackgroundColor === "transparent" ||
-boundTextElement.height / 2, !appState.viewBackgroundColor);
boundTextElement.width, if (!isTransparentHole) {
boundTextElement.height, context.save();
); context.fillStyle = applyDarkModeFilter(
context.scale(1 / appState.exportScale, 1 / appState.exportScale); renderConfig.canvasBackgroundColor,
context.drawImage( renderConfig.theme === THEME.DARK,
tempCanvas, );
-tempCanvas.width / 2, context.fillRect(holeX, holeY, holeWidth, holeHeight);
-tempCanvas.height / 2, context.restore();
tempCanvas.width, } else {
tempCanvas.height, context.clearRect(holeX, holeY, holeWidth, holeHeight);
); }
} else { } else {
context.rotate(element.angle); context.rotate(element.angle);