svg export
This commit is contained in:
parent
7b012b1cad
commit
3f00762a77
@ -9531,12 +9531,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
crop: {
|
crop: null,
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: image.naturalWidth,
|
|
||||||
height: image.naturalHeight,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -32,19 +32,14 @@ import { isInitializedImageElement } from "./typeChecks";
|
|||||||
|
|
||||||
const _cropElement = (
|
const _cropElement = (
|
||||||
element: ExcalidrawImageElement,
|
element: ExcalidrawImageElement,
|
||||||
image: HTMLImageElement,
|
|
||||||
transformHandle: TransformHandleType,
|
transformHandle: TransformHandleType,
|
||||||
naturalWidth: number,
|
naturalWidth: number,
|
||||||
naturalHeight: number,
|
naturalHeight: number,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
const uncroppedWidth =
|
const { width: uncroppedWidth, height: uncroppedHeight } =
|
||||||
element.width /
|
getUncroppedWidthAndHeight(element);
|
||||||
(element.crop ? element.crop.width / image.naturalWidth : 1);
|
|
||||||
const uncroppedHeight =
|
|
||||||
element.height /
|
|
||||||
(element.crop ? element.crop.height / image.naturalHeight : 1);
|
|
||||||
|
|
||||||
const naturalWidthToUncropped = naturalWidth / uncroppedWidth;
|
const naturalWidthToUncropped = naturalWidth / uncroppedWidth;
|
||||||
const naturalHeightToUncropped = naturalHeight / uncroppedHeight;
|
const naturalHeightToUncropped = naturalHeight / uncroppedHeight;
|
||||||
@ -53,7 +48,7 @@ const _cropElement = (
|
|||||||
const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped;
|
const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* uncropped width
|
* uncropped width
|
||||||
* *––––––––––––––––––––––––*
|
* *––––––––––––––––––––––––*
|
||||||
* | (x,y) (natural) |
|
* | (x,y) (natural) |
|
||||||
* | *–––––––* |
|
* | *–––––––* |
|
||||||
@ -79,6 +74,7 @@ const _cropElement = (
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: naturalWidth,
|
width: naturalWidth,
|
||||||
height: naturalHeight,
|
height: naturalHeight,
|
||||||
|
naturalDimension: [naturalWidth, naturalHeight],
|
||||||
};
|
};
|
||||||
|
|
||||||
const previousCropHeight = crop.height;
|
const previousCropHeight = crop.height;
|
||||||
@ -168,25 +164,24 @@ export const cropElement = (
|
|||||||
isInitializedImageElement(element) && imageCache.get(element.fileId)?.image;
|
isInitializedImageElement(element) && imageCache.get(element.fileId)?.image;
|
||||||
|
|
||||||
if (image && !(image instanceof Promise)) {
|
if (image && !(image instanceof Promise)) {
|
||||||
const mutation = _cropElement(
|
mutateElement(
|
||||||
element,
|
element,
|
||||||
image,
|
_cropElement(
|
||||||
transformHandle,
|
element,
|
||||||
image.naturalWidth,
|
transformHandle,
|
||||||
image.naturalHeight,
|
image.naturalWidth,
|
||||||
pointerX,
|
image.naturalHeight,
|
||||||
pointerY,
|
pointerX,
|
||||||
|
pointerY,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
mutateElement(element, mutation);
|
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
updateBoundElements(element, elementsMap, {
|
||||||
oldSize: { width: element.width, height: element.height },
|
oldSize: { width: element.width, height: element.height },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: replace with the refactored resizeSingleElement
|
|
||||||
const recomputeOrigin = (
|
const recomputeOrigin = (
|
||||||
stateAtCropStart: NonDeleted<ExcalidrawElement>,
|
stateAtCropStart: NonDeleted<ExcalidrawElement>,
|
||||||
transformHandle: TransformHandleType,
|
transformHandle: TransformHandleType,
|
||||||
@ -250,87 +245,96 @@ const recomputeOrigin = (
|
|||||||
export const getUncroppedImageElement = (
|
export const getUncroppedImageElement = (
|
||||||
element: ExcalidrawImageElement,
|
element: ExcalidrawImageElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
imageCache: AppClassProperties["imageCache"],
|
|
||||||
) => {
|
) => {
|
||||||
const image =
|
if (element.crop) {
|
||||||
isInitializedImageElement(element) && imageCache.get(element.fileId)?.image;
|
const { width, height } = getUncroppedWidthAndHeight(element);
|
||||||
|
|
||||||
if (image && !(image instanceof Promise)) {
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
if (element.crop) {
|
element,
|
||||||
const width = element.width / (element.crop.width / image.naturalWidth);
|
elementsMap,
|
||||||
const height =
|
);
|
||||||
element.height / (element.crop.height / image.naturalHeight);
|
|
||||||
|
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const topLeftVector = vectorFromPoint(
|
||||||
element,
|
pointRotateRads(pointFrom(x1, y1), pointFrom(cx, cy), element.angle),
|
||||||
elementsMap,
|
);
|
||||||
);
|
const topRightVector = vectorFromPoint(
|
||||||
|
pointRotateRads(pointFrom(x2, y1), pointFrom(cx, cy), element.angle),
|
||||||
|
);
|
||||||
|
const topEdgeNormalized = vectorNormalize(
|
||||||
|
vectorSubtract(topRightVector, topLeftVector),
|
||||||
|
);
|
||||||
|
const bottomLeftVector = vectorFromPoint(
|
||||||
|
pointRotateRads(pointFrom(x1, y2), pointFrom(cx, cy), element.angle),
|
||||||
|
);
|
||||||
|
const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector);
|
||||||
|
const leftEdgeNormalized = vectorNormalize(leftEdgeVector);
|
||||||
|
|
||||||
const topLeftVector = vectorFromPoint(
|
const { cropX, cropY } = adjustCropPosition(element.crop, element.scale);
|
||||||
pointRotateRads(pointFrom(x1, y1), pointFrom(cx, cy), element.angle),
|
|
||||||
);
|
|
||||||
const topRightVector = vectorFromPoint(
|
|
||||||
pointRotateRads(pointFrom(x2, y1), pointFrom(cx, cy), element.angle),
|
|
||||||
);
|
|
||||||
const topEdgeNormalized = vectorNormalize(
|
|
||||||
vectorSubtract(topRightVector, topLeftVector),
|
|
||||||
);
|
|
||||||
const bottomLeftVector = vectorFromPoint(
|
|
||||||
pointRotateRads(pointFrom(x1, y2), pointFrom(cx, cy), element.angle),
|
|
||||||
);
|
|
||||||
const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector);
|
|
||||||
const leftEdgeNormalized = vectorNormalize(leftEdgeVector);
|
|
||||||
|
|
||||||
const { cropX, cropY } = adjustCropPosition(
|
const rotatedTopLeft = vectorAdd(
|
||||||
element.crop,
|
vectorAdd(
|
||||||
element.scale,
|
topLeftVector,
|
||||||
image,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rotatedTopLeft = vectorAdd(
|
|
||||||
vectorAdd(
|
|
||||||
topLeftVector,
|
|
||||||
vectorScale(topEdgeNormalized, (-cropX * width) / image.naturalWidth),
|
|
||||||
),
|
|
||||||
vectorScale(
|
vectorScale(
|
||||||
leftEdgeNormalized,
|
topEdgeNormalized,
|
||||||
(-cropY * height) / image.naturalHeight,
|
(-cropX * width) / element.crop.naturalDimension[0],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
vectorScale(
|
||||||
|
leftEdgeNormalized,
|
||||||
|
(-cropY * height) / element.crop.naturalDimension[1],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const center = pointFromVector(
|
const center = pointFromVector(
|
||||||
vectorAdd(
|
vectorAdd(
|
||||||
vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)),
|
vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)),
|
||||||
vectorScale(leftEdgeNormalized, height / 2),
|
vectorScale(leftEdgeNormalized, height / 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const unrotatedTopLeft = pointRotateRads(
|
const unrotatedTopLeft = pointRotateRads(
|
||||||
pointFromVector(rotatedTopLeft),
|
pointFromVector(rotatedTopLeft),
|
||||||
center,
|
center,
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
const uncroppedElement: ExcalidrawImageElement = {
|
const uncroppedElement: ExcalidrawImageElement = {
|
||||||
...element,
|
...element,
|
||||||
x: unrotatedTopLeft[0],
|
x: unrotatedTopLeft[0],
|
||||||
y: unrotatedTopLeft[1],
|
y: unrotatedTopLeft[1],
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
crop: null,
|
crop: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return uncroppedElement;
|
return uncroppedElement;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUncroppedWidthAndHeight = (element: ExcalidrawImageElement) => {
|
||||||
|
if (element.crop) {
|
||||||
|
const width =
|
||||||
|
element.width / (element.crop.width / element.crop.naturalDimension[0]);
|
||||||
|
const height =
|
||||||
|
element.height / (element.crop.height / element.crop.naturalDimension[1]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: element.width,
|
||||||
|
height: element.height,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const adjustCropPosition = (
|
const adjustCropPosition = (
|
||||||
crop: ImageCrop,
|
crop: ImageCrop,
|
||||||
scale: ExcalidrawImageElement["scale"],
|
scale: ExcalidrawImageElement["scale"],
|
||||||
image: HTMLImageElement,
|
|
||||||
) => {
|
) => {
|
||||||
let cropX = crop.x;
|
let cropX = crop.x;
|
||||||
let cropY = crop.y;
|
let cropY = crop.y;
|
||||||
@ -339,11 +343,11 @@ const adjustCropPosition = (
|
|||||||
const flipY = scale[1] === -1;
|
const flipY = scale[1] === -1;
|
||||||
|
|
||||||
if (flipX) {
|
if (flipX) {
|
||||||
cropX = image.naturalWidth - Math.abs(cropX) - crop.width;
|
cropX = crop.naturalDimension[0] - Math.abs(cropX) - crop.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flipY) {
|
if (flipY) {
|
||||||
cropY = image.naturalHeight - Math.abs(cropY) - crop.height;
|
cropY = crop.naturalDimension[1] - Math.abs(cropY) - crop.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -137,6 +137,7 @@ export type ImageCrop = {
|
|||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
naturalDimension: [number, number];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
||||||
@ -147,7 +148,7 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
|||||||
status: "pending" | "saved" | "error";
|
status: "pending" | "saved" | "error";
|
||||||
/** X and Y scale factors <-1, 1>, used for image axis flipping */
|
/** X and Y scale factors <-1, 1>, used for image axis flipping */
|
||||||
scale: [number, number];
|
scale: [number, number];
|
||||||
|
/** whether an element is cropped */
|
||||||
crop: ImageCrop | null;
|
crop: ImageCrop | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -950,11 +950,7 @@ export const renderElement = (
|
|||||||
context.globalAlpha = 0.1;
|
context.globalAlpha = 0.1;
|
||||||
|
|
||||||
const uncroppedElementCanvas = generateElementCanvas(
|
const uncroppedElementCanvas = generateElementCanvas(
|
||||||
getUncroppedImageElement(
|
getUncroppedImageElement(elementWithCanvas.element, elementsMap),
|
||||||
elementWithCanvas.element,
|
|
||||||
elementsMap,
|
|
||||||
renderConfig.imageCache,
|
|
||||||
),
|
|
||||||
allElementsMap,
|
allElementsMap,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
@ -37,6 +37,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
|||||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
import { getUncroppedWidthAndHeight } from "../element/cropElement";
|
||||||
|
|
||||||
const roughSVGDrawWithPrecision = (
|
const roughSVGDrawWithPrecision = (
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
@ -417,12 +418,30 @@ const renderElementToSvg = (
|
|||||||
symbol.id = symbolId;
|
symbol.id = symbolId;
|
||||||
|
|
||||||
const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image");
|
const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image");
|
||||||
|
|
||||||
image.setAttribute("width", "100%");
|
|
||||||
image.setAttribute("height", "100%");
|
|
||||||
image.setAttribute("href", fileData.dataURL);
|
image.setAttribute("href", fileData.dataURL);
|
||||||
image.setAttribute("preserveAspectRatio", "none");
|
image.setAttribute("preserveAspectRatio", "none");
|
||||||
|
|
||||||
|
if (element.crop) {
|
||||||
|
const { width: uncroppedWidth, height: uncroppedHeight } =
|
||||||
|
getUncroppedWidthAndHeight(element);
|
||||||
|
|
||||||
|
symbol.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
`${
|
||||||
|
element.crop.x /
|
||||||
|
(element.crop.naturalDimension[0] / uncroppedWidth)
|
||||||
|
} ${
|
||||||
|
element.crop.y /
|
||||||
|
(element.crop.naturalDimension[1] / uncroppedHeight)
|
||||||
|
} ${width} ${height}`,
|
||||||
|
);
|
||||||
|
image.setAttribute("width", `${uncroppedWidth}`);
|
||||||
|
image.setAttribute("height", `${uncroppedHeight}`);
|
||||||
|
} else {
|
||||||
|
image.setAttribute("width", "100%");
|
||||||
|
image.setAttribute("height", "100%");
|
||||||
|
}
|
||||||
|
|
||||||
symbol.appendChild(image);
|
symbol.appendChild(image);
|
||||||
|
|
||||||
root.prepend(symbol);
|
root.prepend(symbol);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user