@excalidraw/excalidraw 0.17.1-7500-ac247a0 → 0.17.1-a38e82f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -2
- package/dist/browser/dev/excalidraw-assets-dev/chunk-5VWQDKDR.js +20279 -0
- package/dist/browser/dev/excalidraw-assets-dev/chunk-5VWQDKDR.js.map +7 -0
- package/dist/browser/dev/excalidraw-assets-dev/{chunk-2W5GQUR4.js → chunk-IM4WTX2M.js} +12 -6
- package/dist/browser/dev/excalidraw-assets-dev/chunk-IM4WTX2M.js.map +7 -0
- package/dist/browser/dev/excalidraw-assets-dev/{en-OC6JWP3X.js → en-IOBA4CS2.js} +4 -2
- package/dist/browser/dev/excalidraw-assets-dev/image-LK4UNFRZ.css +6 -0
- package/dist/browser/dev/excalidraw-assets-dev/image-LK4UNFRZ.css.map +7 -0
- package/dist/browser/dev/excalidraw-assets-dev/{image-5TVMINCA.js → image-VKDAL6BQ.js} +2 -4
- package/dist/browser/dev/excalidraw-assets-dev/roundRect-T5BX56ZF.js +161 -0
- package/dist/browser/dev/excalidraw-assets-dev/roundRect-T5BX56ZF.js.map +7 -0
- package/dist/browser/dev/index.css +189 -129
- package/dist/browser/dev/index.css.map +3 -3
- package/dist/browser/dev/index.js +34708 -37
- package/dist/browser/dev/index.js.map +4 -4
- package/dist/browser/prod/excalidraw-assets/chunk-LIG3S5TN.js +11 -0
- package/dist/browser/prod/excalidraw-assets/chunk-N2C5DK3B.js +55 -0
- package/dist/browser/prod/excalidraw-assets/en-WFZVQ7I6.js +1 -0
- package/dist/browser/prod/excalidraw-assets/image-4AT7LYMR.js +1 -0
- package/dist/browser/prod/excalidraw-assets/image-X66R2EM5.css +1 -0
- package/dist/browser/prod/excalidraw-assets/roundRect-2ACQK4DA.js +1 -0
- package/dist/browser/prod/index.css +1 -1
- package/dist/browser/prod/index.js +203 -1
- package/dist/{prod/en-RLIAOBCI.json → dev/en-TDNWCAOT.json} +9 -5
- package/dist/dev/index.css +189 -129
- package/dist/dev/index.css.map +3 -3
- package/dist/dev/index.js +39078 -40080
- package/dist/dev/index.js.map +4 -4
- package/dist/excalidraw/actions/actionAddToLibrary.d.ts +15 -15
- package/dist/excalidraw/actions/actionAlign.d.ts +6 -6
- package/dist/excalidraw/actions/actionAlign.js +2 -1
- package/dist/excalidraw/actions/actionBoundText.d.ts +10 -10
- package/dist/excalidraw/actions/actionBoundText.js +8 -8
- package/dist/excalidraw/actions/actionCanvas.d.ts +58 -58
- package/dist/excalidraw/actions/actionClipboard.d.ts +34 -34
- package/dist/excalidraw/actions/actionClipboard.js +9 -2
- package/dist/excalidraw/actions/actionDeleteSelected.d.ts +15 -15
- package/dist/excalidraw/actions/actionDeleteSelected.js +3 -2
- package/dist/excalidraw/actions/actionDistribute.d.ts +2 -2
- package/dist/excalidraw/actions/actionDistribute.js +1 -1
- package/dist/excalidraw/actions/actionDuplicateSelection.d.ts +1 -1
- package/dist/excalidraw/actions/actionDuplicateSelection.js +4 -3
- package/dist/excalidraw/actions/actionElementLock.d.ts +10 -10
- package/dist/excalidraw/actions/actionExport.d.ts +43 -43
- package/dist/excalidraw/actions/actionExport.js +4 -4
- package/dist/excalidraw/actions/actionFinalize.d.ts +9 -9
- package/dist/excalidraw/actions/actionFinalize.js +7 -6
- package/dist/excalidraw/actions/actionFlip.d.ts +2 -2
- package/dist/excalidraw/actions/actionFlip.js +11 -11
- package/dist/excalidraw/actions/actionFrame.d.ts +16 -16
- package/dist/excalidraw/actions/actionFrame.js +1 -1
- package/dist/excalidraw/actions/actionGroup.d.ts +10 -10
- package/dist/excalidraw/actions/actionGroup.js +3 -2
- package/dist/excalidraw/actions/actionLinearEditor.d.ts +5 -5
- package/dist/excalidraw/actions/actionLinearEditor.js +1 -1
- package/dist/excalidraw/{element/Hyperlink.d.ts → actions/actionLink.d.ts} +29 -51
- package/dist/excalidraw/actions/actionLink.js +40 -0
- package/dist/excalidraw/actions/actionMenu.d.ts +13 -13
- package/dist/excalidraw/actions/actionNavigate.d.ts +10 -10
- package/dist/excalidraw/actions/actionNavigate.js +1 -1
- package/dist/excalidraw/actions/actionProperties.d.ts +77 -77
- package/dist/excalidraw/actions/actionProperties.js +32 -27
- package/dist/excalidraw/actions/actionSelectAll.d.ts +5 -5
- package/dist/excalidraw/actions/actionSelectAll.js +1 -1
- package/dist/excalidraw/actions/actionStyles.d.ts +7 -7
- package/dist/excalidraw/actions/actionStyles.js +4 -4
- package/dist/excalidraw/actions/actionToggleGridMode.d.ts +5 -5
- package/dist/excalidraw/actions/actionToggleObjectsSnapMode.d.ts +5 -5
- package/dist/excalidraw/actions/actionToggleStats.d.ts +5 -5
- package/dist/excalidraw/actions/actionToggleViewMode.d.ts +5 -5
- package/dist/excalidraw/actions/actionToggleZenMode.d.ts +5 -5
- package/dist/excalidraw/actions/index.d.ts +1 -1
- package/dist/excalidraw/actions/index.js +1 -1
- package/dist/excalidraw/actions/manager.js +2 -1
- package/dist/excalidraw/align.d.ts +2 -2
- package/dist/excalidraw/align.js +2 -2
- package/dist/excalidraw/animated-trail.d.ts +33 -0
- package/dist/excalidraw/animated-trail.js +96 -0
- package/dist/excalidraw/animation-frame-handler.d.ts +16 -0
- package/dist/excalidraw/animation-frame-handler.js +55 -0
- package/dist/excalidraw/appState.d.ts +1 -1
- package/dist/excalidraw/appState.js +1 -3
- package/dist/excalidraw/clipboard.js +5 -5
- package/dist/excalidraw/components/Actions.d.ts +3 -3
- package/dist/excalidraw/components/Actions.js +18 -7
- package/dist/excalidraw/components/App.d.ts +23 -16
- package/dist/excalidraw/components/App.js +387 -272
- package/dist/excalidraw/components/Button.d.ts +1 -1
- package/dist/excalidraw/components/FilledButton.d.ts +2 -2
- package/dist/excalidraw/components/FilledButton.js +27 -3
- package/dist/excalidraw/components/FollowMode/FollowMode.js +1 -1
- package/dist/excalidraw/components/ImageExportDialog.d.ts +2 -1
- package/dist/excalidraw/components/ImageExportDialog.js +17 -13
- package/dist/excalidraw/components/JSONExportDialog.js +1 -1
- package/dist/excalidraw/components/{LaserTool/LaserPointerButton.d.ts → LaserPointerButton.d.ts} +1 -1
- package/dist/excalidraw/components/{LaserTool/LaserPointerButton.js → LaserPointerButton.js} +2 -2
- package/dist/excalidraw/components/LayerUI.js +3 -3
- package/dist/excalidraw/components/MobileMenu.js +1 -1
- package/dist/excalidraw/components/ProjectName.d.ts +0 -1
- package/dist/excalidraw/components/ProjectName.js +1 -1
- package/dist/excalidraw/components/PublishLibrary.js +1 -1
- package/dist/excalidraw/components/SVGLayer.d.ts +8 -0
- package/dist/excalidraw/components/SVGLayer.js +20 -0
- package/dist/excalidraw/components/ShareableLinkDialog.js +10 -10
- package/dist/excalidraw/components/Sidebar/Sidebar.d.ts +1 -1
- package/dist/excalidraw/components/Stack.d.ts +2 -2
- package/dist/excalidraw/components/TTDDialog/common.js +10 -1
- package/dist/excalidraw/components/TextField.d.ts +5 -2
- package/dist/excalidraw/components/TextField.js +6 -3
- package/dist/excalidraw/components/Toast.d.ts +3 -2
- package/dist/excalidraw/components/Toast.js +2 -2
- package/dist/excalidraw/components/ToolButton.js +2 -1
- package/dist/excalidraw/components/canvases/InteractiveCanvas.d.ts +2 -2
- package/dist/excalidraw/components/canvases/InteractiveCanvas.js +6 -5
- package/dist/excalidraw/components/canvases/StaticCanvas.d.ts +4 -3
- package/dist/excalidraw/components/canvases/StaticCanvas.js +7 -5
- package/dist/excalidraw/components/dropdownMenu/DropdownMenuContent.js +22 -2
- package/dist/excalidraw/components/hyperlink/Hyperlink.d.ts +19 -0
- package/dist/excalidraw/{element → components/hyperlink}/Hyperlink.js +40 -115
- package/dist/excalidraw/components/hyperlink/helpers.d.ts +7 -0
- package/dist/excalidraw/components/hyperlink/helpers.js +49 -0
- package/dist/excalidraw/components/icons.d.ts +2 -1
- package/dist/excalidraw/components/icons.js +2 -1
- package/dist/excalidraw/components/live-collaboration/LiveCollaborationTrigger.js +3 -2
- package/dist/excalidraw/components/main-menu/DefaultItems.js +5 -2
- package/dist/excalidraw/constants.d.ts +6 -0
- package/dist/excalidraw/constants.js +6 -0
- package/dist/excalidraw/data/blob.js +13 -14
- package/dist/excalidraw/data/filesystem.d.ts +1 -1
- package/dist/excalidraw/data/index.d.ts +2 -1
- package/dist/excalidraw/data/index.js +20 -16
- package/dist/excalidraw/data/json.d.ts +1 -1
- package/dist/excalidraw/data/json.js +5 -3
- package/dist/excalidraw/data/resave.d.ts +1 -1
- package/dist/excalidraw/data/resave.js +2 -2
- package/dist/excalidraw/data/restore.js +8 -13
- package/dist/excalidraw/data/transform.js +13 -9
- package/dist/excalidraw/distribute.d.ts +2 -2
- package/dist/excalidraw/distribute.js +2 -2
- package/dist/excalidraw/element/ElementCanvasButtons.d.ts +3 -2
- package/dist/excalidraw/element/ElementCanvasButtons.js +4 -4
- package/dist/excalidraw/element/binding.d.ts +9 -9
- package/dist/excalidraw/element/binding.js +61 -59
- package/dist/excalidraw/element/bounds.d.ts +5 -5
- package/dist/excalidraw/element/bounds.js +29 -32
- package/dist/excalidraw/element/collision.d.ts +11 -11
- package/dist/excalidraw/element/collision.js +49 -46
- package/dist/excalidraw/element/containerCache.d.ts +11 -0
- package/dist/excalidraw/element/containerCache.js +14 -0
- package/dist/excalidraw/element/dragElements.js +10 -19
- package/dist/excalidraw/element/embeddable.d.ts +12 -13
- package/dist/excalidraw/element/embeddable.js +17 -27
- package/dist/excalidraw/element/image.js +1 -2
- package/dist/excalidraw/element/index.d.ts +0 -1
- package/dist/excalidraw/element/index.js +0 -1
- package/dist/excalidraw/element/linearElementEditor.d.ts +36 -36
- package/dist/excalidraw/element/linearElementEditor.js +79 -80
- package/dist/excalidraw/element/newElement.d.ts +4 -6
- package/dist/excalidraw/element/newElement.js +11 -16
- package/dist/excalidraw/element/resizeElements.d.ts +6 -6
- package/dist/excalidraw/element/resizeElements.js +40 -46
- package/dist/excalidraw/element/resizeTest.d.ts +3 -3
- package/dist/excalidraw/element/resizeTest.js +4 -4
- package/dist/excalidraw/element/sizeHelpers.d.ts +2 -2
- package/dist/excalidraw/element/sizeHelpers.js +2 -2
- package/dist/excalidraw/element/textElement.d.ts +18 -20
- package/dist/excalidraw/element/textElement.js +80 -111
- package/dist/excalidraw/element/textWysiwyg.d.ts +1 -6
- package/dist/excalidraw/element/textWysiwyg.js +15 -37
- package/dist/excalidraw/element/transformHandles.d.ts +4 -4
- package/dist/excalidraw/element/transformHandles.js +6 -6
- package/dist/excalidraw/element/typeChecks.js +4 -1
- package/dist/excalidraw/element/types.d.ts +24 -11
- package/dist/excalidraw/frame.d.ts +26 -20
- package/dist/excalidraw/frame.js +157 -84
- package/dist/excalidraw/groups.d.ts +3 -3
- package/dist/excalidraw/groups.js +11 -3
- package/dist/excalidraw/history.d.ts +1 -1
- package/dist/excalidraw/hooks/useLibraryItemSvg.js +1 -1
- package/dist/excalidraw/index.d.ts +8 -9
- package/dist/excalidraw/index.js +15 -11
- package/dist/excalidraw/laser-trails.d.ts +19 -0
- package/dist/excalidraw/laser-trails.js +95 -0
- package/dist/excalidraw/locales/en.json +9 -5
- package/dist/excalidraw/reactUtils.d.ts +14 -0
- package/dist/excalidraw/reactUtils.js +45 -0
- package/dist/excalidraw/renderer/helpers.d.ts +13 -0
- package/dist/excalidraw/renderer/helpers.js +39 -0
- package/dist/excalidraw/renderer/interactiveScene.d.ts +20 -0
- package/dist/excalidraw/renderer/{renderScene.js → interactiveScene.js} +199 -474
- package/dist/excalidraw/renderer/renderElement.d.ts +6 -6
- package/dist/excalidraw/renderer/renderElement.js +54 -366
- package/dist/excalidraw/renderer/staticScene.d.ts +11 -0
- package/dist/excalidraw/renderer/staticScene.js +205 -0
- package/dist/excalidraw/renderer/staticSvgScene.d.ts +5 -0
- package/dist/excalidraw/renderer/staticSvgScene.js +385 -0
- package/dist/excalidraw/scene/Fonts.js +2 -1
- package/dist/excalidraw/scene/Renderer.d.ts +1 -1
- package/dist/excalidraw/scene/Renderer.js +32 -20
- package/dist/excalidraw/scene/Scene.d.ts +10 -9
- package/dist/excalidraw/scene/Scene.js +45 -21
- package/dist/excalidraw/scene/Shape.d.ts +3 -1
- package/dist/excalidraw/scene/Shape.js +7 -5
- package/dist/excalidraw/scene/ShapeCache.d.ts +2 -1
- package/dist/excalidraw/scene/ShapeCache.js +1 -0
- package/dist/excalidraw/scene/comparisons.js +2 -1
- package/dist/excalidraw/scene/export.d.ts +3 -0
- package/dist/excalidraw/scene/export.js +20 -40
- package/dist/excalidraw/scene/index.d.ts +0 -1
- package/dist/excalidraw/scene/index.js +0 -1
- package/dist/excalidraw/scene/scrollbars.d.ts +1 -1
- package/dist/excalidraw/scene/scrollbars.js +1 -1
- package/dist/excalidraw/scene/selection.d.ts +5 -5
- package/dist/excalidraw/scene/selection.js +16 -14
- package/dist/excalidraw/scene/types.d.ts +11 -5
- package/dist/excalidraw/snapping.d.ts +7 -7
- package/dist/excalidraw/snapping.js +21 -20
- package/dist/excalidraw/types.d.ts +10 -11
- package/dist/excalidraw/utility-types.d.ts +5 -0
- package/dist/excalidraw/utils.d.ts +18 -15
- package/dist/excalidraw/utils.js +37 -45
- package/dist/{dev/en-RLIAOBCI.json → prod/en-TDNWCAOT.json} +9 -5
- package/dist/prod/index.css +1 -1
- package/dist/prod/index.js +42 -42
- package/dist/utils/bbox.d.ts +2 -2
- package/dist/utils/export.d.ts +3 -3
- package/dist/utils/export.js +3 -13
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +2 -2
- package/dist/utils/withinBounds.d.ts +1 -1
- package/dist/utils/withinBounds.js +5 -2
- package/package.json +4 -4
- package/dist/browser/dev/excalidraw-assets-dev/chunk-2W5GQUR4.js.map +0 -7
- package/dist/browser/dev/excalidraw-assets-dev/chunk-KGZXLFLR.js +0 -53497
- package/dist/browser/dev/excalidraw-assets-dev/chunk-KGZXLFLR.js.map +0 -7
- package/dist/browser/dev/excalidraw-assets-dev/image-3MFRCKYM.css +0 -5797
- package/dist/browser/dev/excalidraw-assets-dev/image-3MFRCKYM.css.map +0 -7
- package/dist/browser/prod/excalidraw-assets/chunk-4YN2HN3S.js +0 -257
- package/dist/browser/prod/excalidraw-assets/chunk-OWLL6VOG.js +0 -11
- package/dist/browser/prod/excalidraw-assets/en-ERQOR3OC.js +0 -1
- package/dist/browser/prod/excalidraw-assets/image-LTLHTTSE.js +0 -1
- package/dist/browser/prod/excalidraw-assets/image-QBL334OA.css +0 -1
- package/dist/excalidraw/components/LaserTool/LaserPathManager.d.ts +0 -28
- package/dist/excalidraw/components/LaserTool/LaserPathManager.js +0 -225
- package/dist/excalidraw/components/LaserTool/LaserTool.d.ts +0 -8
- package/dist/excalidraw/components/LaserTool/LaserTool.js +0 -15
- package/dist/excalidraw/renderer/renderScene.d.ts +0 -25
- package/dist/excalidraw/vite.config.d.mts +0 -2
- package/dist/excalidraw/vite.config.mjs +0 -13
- /package/dist/browser/dev/excalidraw-assets-dev/{en-OC6JWP3X.js.map → en-IOBA4CS2.js.map} +0 -0
- /package/dist/browser/dev/excalidraw-assets-dev/{image-5TVMINCA.js.map → image-VKDAL6BQ.js.map} +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { FRAME_STYLE } from "../constants";
|
|
2
|
+
import { getElementAbsoluteCoords } from "../element";
|
|
3
|
+
import { elementOverlapsWithFrame, getTargetFrame, isElementInFrame, } from "../frame";
|
|
4
|
+
import { isEmbeddableElement, isIframeLikeElement, } from "../element/typeChecks";
|
|
5
|
+
import { renderElement } from "../renderer/renderElement";
|
|
6
|
+
import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
|
|
7
|
+
import { EXTERNAL_LINK_IMG, getLinkHandleFromCoords, } from "../components/hyperlink/helpers";
|
|
8
|
+
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
|
|
9
|
+
import { throttleRAF } from "../utils";
|
|
10
|
+
const strokeGrid = (context, gridSize, scrollX, scrollY, zoom, width, height) => {
|
|
11
|
+
const BOLD_LINE_FREQUENCY = 5;
|
|
12
|
+
let GridLineColor;
|
|
13
|
+
(function (GridLineColor) {
|
|
14
|
+
GridLineColor["Bold"] = "#cccccc";
|
|
15
|
+
GridLineColor["Regular"] = "#e5e5e5";
|
|
16
|
+
})(GridLineColor || (GridLineColor = {}));
|
|
17
|
+
const offsetX = -Math.round(zoom.value / gridSize) * gridSize + (scrollX % gridSize);
|
|
18
|
+
const offsetY = -Math.round(zoom.value / gridSize) * gridSize + (scrollY % gridSize);
|
|
19
|
+
const lineWidth = Math.min(1 / zoom.value, 1);
|
|
20
|
+
const spaceWidth = 1 / zoom.value;
|
|
21
|
+
const lineDash = [lineWidth * 3, spaceWidth + (lineWidth + spaceWidth)];
|
|
22
|
+
context.save();
|
|
23
|
+
context.lineWidth = lineWidth;
|
|
24
|
+
for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
|
|
25
|
+
const isBold = Math.round(x - scrollX) % (BOLD_LINE_FREQUENCY * gridSize) === 0;
|
|
26
|
+
context.beginPath();
|
|
27
|
+
context.setLineDash(isBold ? [] : lineDash);
|
|
28
|
+
context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
|
|
29
|
+
context.moveTo(x, offsetY - gridSize);
|
|
30
|
+
context.lineTo(x, offsetY + height + gridSize * 2);
|
|
31
|
+
context.stroke();
|
|
32
|
+
}
|
|
33
|
+
for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
|
|
34
|
+
const isBold = Math.round(y - scrollY) % (BOLD_LINE_FREQUENCY * gridSize) === 0;
|
|
35
|
+
context.beginPath();
|
|
36
|
+
context.setLineDash(isBold ? [] : lineDash);
|
|
37
|
+
context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
|
|
38
|
+
context.moveTo(offsetX - gridSize, y);
|
|
39
|
+
context.lineTo(offsetX + width + gridSize * 2, y);
|
|
40
|
+
context.stroke();
|
|
41
|
+
}
|
|
42
|
+
context.restore();
|
|
43
|
+
};
|
|
44
|
+
const frameClip = (frame, context, renderConfig, appState) => {
|
|
45
|
+
context.translate(frame.x + appState.scrollX, frame.y + appState.scrollY);
|
|
46
|
+
context.beginPath();
|
|
47
|
+
if (context.roundRect) {
|
|
48
|
+
context.roundRect(0, 0, frame.width, frame.height, FRAME_STYLE.radius / appState.zoom.value);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
context.rect(0, 0, frame.width, frame.height);
|
|
52
|
+
}
|
|
53
|
+
context.clip();
|
|
54
|
+
context.translate(-(frame.x + appState.scrollX), -(frame.y + appState.scrollY));
|
|
55
|
+
};
|
|
56
|
+
let linkCanvasCache;
|
|
57
|
+
const renderLinkIcon = (element, context, appState, elementsMap) => {
|
|
58
|
+
if (element.link && !appState.selectedElementIds[element.id]) {
|
|
59
|
+
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
|
60
|
+
const [x, y, width, height] = getLinkHandleFromCoords([x1, y1, x2, y2], element.angle, appState);
|
|
61
|
+
const centerX = x + width / 2;
|
|
62
|
+
const centerY = y + height / 2;
|
|
63
|
+
context.save();
|
|
64
|
+
context.translate(appState.scrollX + centerX, appState.scrollY + centerY);
|
|
65
|
+
context.rotate(element.angle);
|
|
66
|
+
if (!linkCanvasCache || linkCanvasCache.zoom !== appState.zoom.value) {
|
|
67
|
+
linkCanvasCache = document.createElement("canvas");
|
|
68
|
+
linkCanvasCache.zoom = appState.zoom.value;
|
|
69
|
+
linkCanvasCache.width =
|
|
70
|
+
width * window.devicePixelRatio * appState.zoom.value;
|
|
71
|
+
linkCanvasCache.height =
|
|
72
|
+
height * window.devicePixelRatio * appState.zoom.value;
|
|
73
|
+
const linkCanvasCacheContext = linkCanvasCache.getContext("2d");
|
|
74
|
+
linkCanvasCacheContext.scale(window.devicePixelRatio * appState.zoom.value, window.devicePixelRatio * appState.zoom.value);
|
|
75
|
+
linkCanvasCacheContext.fillStyle = "#fff";
|
|
76
|
+
linkCanvasCacheContext.fillRect(0, 0, width, height);
|
|
77
|
+
linkCanvasCacheContext.drawImage(EXTERNAL_LINK_IMG, 0, 0, width, height);
|
|
78
|
+
linkCanvasCacheContext.restore();
|
|
79
|
+
context.drawImage(linkCanvasCache, x - centerX, y - centerY, width, height);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
context.drawImage(linkCanvasCache, x - centerX, y - centerY, width, height);
|
|
83
|
+
}
|
|
84
|
+
context.restore();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const _renderStaticScene = ({ canvas, rc, elementsMap, allElementsMap, visibleElements, scale, appState, renderConfig, }) => {
|
|
88
|
+
if (canvas === null) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const { renderGrid = true, isExporting } = renderConfig;
|
|
92
|
+
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(canvas, scale);
|
|
93
|
+
const context = bootstrapCanvas({
|
|
94
|
+
canvas,
|
|
95
|
+
scale,
|
|
96
|
+
normalizedWidth,
|
|
97
|
+
normalizedHeight,
|
|
98
|
+
theme: appState.theme,
|
|
99
|
+
isExporting,
|
|
100
|
+
viewBackgroundColor: appState.viewBackgroundColor,
|
|
101
|
+
});
|
|
102
|
+
// Apply zoom
|
|
103
|
+
context.scale(appState.zoom.value, appState.zoom.value);
|
|
104
|
+
// Grid
|
|
105
|
+
if (renderGrid && appState.gridSize) {
|
|
106
|
+
strokeGrid(context, appState.gridSize, appState.scrollX, appState.scrollY, appState.zoom, normalizedWidth / appState.zoom.value, normalizedHeight / appState.zoom.value);
|
|
107
|
+
}
|
|
108
|
+
const groupsToBeAddedToFrame = new Set();
|
|
109
|
+
visibleElements.forEach((element) => {
|
|
110
|
+
if (element.groupIds.length > 0 &&
|
|
111
|
+
appState.frameToHighlight &&
|
|
112
|
+
appState.selectedElementIds[element.id] &&
|
|
113
|
+
(elementOverlapsWithFrame(element, appState.frameToHighlight, elementsMap) ||
|
|
114
|
+
element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId)))) {
|
|
115
|
+
element.groupIds.forEach((groupId) => groupsToBeAddedToFrame.add(groupId));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// Paint visible elements
|
|
119
|
+
visibleElements
|
|
120
|
+
.filter((el) => !isIframeLikeElement(el))
|
|
121
|
+
.forEach((element) => {
|
|
122
|
+
try {
|
|
123
|
+
const frameId = element.frameId || appState.frameToHighlight?.id;
|
|
124
|
+
if (frameId &&
|
|
125
|
+
appState.frameRendering.enabled &&
|
|
126
|
+
appState.frameRendering.clip) {
|
|
127
|
+
context.save();
|
|
128
|
+
const frame = getTargetFrame(element, elementsMap, appState);
|
|
129
|
+
// TODO do we need to check isElementInFrame here?
|
|
130
|
+
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
|
131
|
+
frameClip(frame, context, renderConfig, appState);
|
|
132
|
+
}
|
|
133
|
+
renderElement(element, elementsMap, allElementsMap, rc, context, renderConfig, appState);
|
|
134
|
+
context.restore();
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
renderElement(element, elementsMap, allElementsMap, rc, context, renderConfig, appState);
|
|
138
|
+
}
|
|
139
|
+
if (!isExporting) {
|
|
140
|
+
renderLinkIcon(element, context, appState, elementsMap);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// render embeddables on top
|
|
148
|
+
visibleElements
|
|
149
|
+
.filter((el) => isIframeLikeElement(el))
|
|
150
|
+
.forEach((element) => {
|
|
151
|
+
try {
|
|
152
|
+
const render = () => {
|
|
153
|
+
renderElement(element, elementsMap, allElementsMap, rc, context, renderConfig, appState);
|
|
154
|
+
if (isIframeLikeElement(element) &&
|
|
155
|
+
(isExporting ||
|
|
156
|
+
(isEmbeddableElement(element) &&
|
|
157
|
+
renderConfig.embedsValidationStatus.get(element.id) !==
|
|
158
|
+
true)) &&
|
|
159
|
+
element.width &&
|
|
160
|
+
element.height) {
|
|
161
|
+
const label = createPlaceholderEmbeddableLabel(element);
|
|
162
|
+
renderElement(label, elementsMap, allElementsMap, rc, context, renderConfig, appState);
|
|
163
|
+
}
|
|
164
|
+
if (!isExporting) {
|
|
165
|
+
renderLinkIcon(element, context, appState, elementsMap);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
// - when exporting the whole canvas, we DO NOT apply clipping
|
|
169
|
+
// - when we are exporting a particular frame, apply clipping
|
|
170
|
+
// if the containing frame is not selected, apply clipping
|
|
171
|
+
const frameId = element.frameId || appState.frameToHighlight?.id;
|
|
172
|
+
if (frameId &&
|
|
173
|
+
appState.frameRendering.enabled &&
|
|
174
|
+
appState.frameRendering.clip) {
|
|
175
|
+
context.save();
|
|
176
|
+
const frame = getTargetFrame(element, elementsMap, appState);
|
|
177
|
+
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
|
178
|
+
frameClip(frame, context, renderConfig, appState);
|
|
179
|
+
}
|
|
180
|
+
render();
|
|
181
|
+
context.restore();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
render();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error(error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
/** throttled to animation framerate */
|
|
193
|
+
export const renderStaticSceneThrottled = throttleRAF((config) => {
|
|
194
|
+
_renderStaticScene(config);
|
|
195
|
+
}, { trailing: true });
|
|
196
|
+
/**
|
|
197
|
+
* Static scene is the non-ui canvas where we render elements.
|
|
198
|
+
*/
|
|
199
|
+
export const renderStaticScene = (renderConfig, throttle) => {
|
|
200
|
+
if (throttle) {
|
|
201
|
+
renderStaticSceneThrottled(renderConfig);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
_renderStaticScene(renderConfig);
|
|
205
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { RoughSVG } from "roughjs/bin/svg";
|
|
2
|
+
import { NonDeletedExcalidrawElement } from "../element/types";
|
|
3
|
+
import { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
|
|
4
|
+
import { BinaryFiles } from "../types";
|
|
5
|
+
export declare const renderSceneToSvg: (elements: readonly NonDeletedExcalidrawElement[], elementsMap: RenderableElementsMap, rsvg: RoughSVG, svgRoot: SVGElement, files: BinaryFiles, renderConfig: SVGRenderConfig) => void;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { FRAME_STYLE, MAX_DECIMALS_FOR_SVG_EXPORT, MIME_TYPES, SVG_NS, } from "../constants";
|
|
2
|
+
import { normalizeLink, toValidURL } from "../data/url";
|
|
3
|
+
import { getElementAbsoluteCoords } from "../element";
|
|
4
|
+
import { createPlaceholderEmbeddableLabel, getEmbedLink, } from "../element/embeddable";
|
|
5
|
+
import { LinearElementEditor } from "../element/linearElementEditor";
|
|
6
|
+
import { getBoundTextElement, getContainerElement, getLineHeightInPx, getVerticalOffset, } from "../element/textElement";
|
|
7
|
+
import { isArrowElement, isIframeLikeElement, isInitializedImageElement, isTextElement, } from "../element/typeChecks";
|
|
8
|
+
import { getContainingFrame } from "../frame";
|
|
9
|
+
import { getCornerRadius, isPathALoop } from "../math";
|
|
10
|
+
import { ShapeCache } from "../scene/ShapeCache";
|
|
11
|
+
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
|
12
|
+
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
|
13
|
+
const roughSVGDrawWithPrecision = (rsvg, drawable, precision) => {
|
|
14
|
+
if (typeof precision === "undefined") {
|
|
15
|
+
return rsvg.draw(drawable);
|
|
16
|
+
}
|
|
17
|
+
const pshape = {
|
|
18
|
+
sets: drawable.sets,
|
|
19
|
+
shape: drawable.shape,
|
|
20
|
+
options: { ...drawable.options, fixedDecimalPlaceDigits: precision },
|
|
21
|
+
};
|
|
22
|
+
return rsvg.draw(pshape);
|
|
23
|
+
};
|
|
24
|
+
const maybeWrapNodesInFrameClipPath = (element, root, nodes, frameRendering, elementsMap) => {
|
|
25
|
+
if (!frameRendering.enabled || !frameRendering.clip) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const frame = getContainingFrame(element, elementsMap);
|
|
29
|
+
if (frame) {
|
|
30
|
+
const g = root.ownerDocument.createElementNS(SVG_NS, "g");
|
|
31
|
+
g.setAttributeNS(SVG_NS, "clip-path", `url(#${frame.id})`);
|
|
32
|
+
nodes.forEach((node) => g.appendChild(node));
|
|
33
|
+
return g;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
const renderElementToSvg = (element, elementsMap, rsvg, svgRoot, files, offsetX, offsetY, renderConfig) => {
|
|
38
|
+
const offset = { x: offsetX, y: offsetY };
|
|
39
|
+
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
|
40
|
+
let cx = (x2 - x1) / 2 - (element.x - x1);
|
|
41
|
+
let cy = (y2 - y1) / 2 - (element.y - y1);
|
|
42
|
+
if (isTextElement(element)) {
|
|
43
|
+
const container = getContainerElement(element, elementsMap);
|
|
44
|
+
if (isArrowElement(container)) {
|
|
45
|
+
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container, elementsMap);
|
|
46
|
+
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(container, element, elementsMap);
|
|
47
|
+
cx = (x2 - x1) / 2 - (boundTextCoords.x - x1);
|
|
48
|
+
cy = (y2 - y1) / 2 - (boundTextCoords.y - y1);
|
|
49
|
+
offsetX = offsetX + boundTextCoords.x - element.x;
|
|
50
|
+
offsetY = offsetY + boundTextCoords.y - element.y;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const degree = (180 * element.angle) / Math.PI;
|
|
54
|
+
// element to append node to, most of the time svgRoot
|
|
55
|
+
let root = svgRoot;
|
|
56
|
+
// if the element has a link, create an anchor tag and make that the new root
|
|
57
|
+
if (element.link) {
|
|
58
|
+
const anchorTag = svgRoot.ownerDocument.createElementNS(SVG_NS, "a");
|
|
59
|
+
anchorTag.setAttribute("href", normalizeLink(element.link));
|
|
60
|
+
root.appendChild(anchorTag);
|
|
61
|
+
root = anchorTag;
|
|
62
|
+
}
|
|
63
|
+
const addToRoot = (node, element) => {
|
|
64
|
+
if (isTestEnv()) {
|
|
65
|
+
node.setAttribute("data-id", element.id);
|
|
66
|
+
}
|
|
67
|
+
root.appendChild(node);
|
|
68
|
+
};
|
|
69
|
+
const opacity = ((getContainingFrame(element, elementsMap)?.opacity ?? 100) *
|
|
70
|
+
element.opacity) /
|
|
71
|
+
10000;
|
|
72
|
+
switch (element.type) {
|
|
73
|
+
case "selection": {
|
|
74
|
+
// Since this is used only during editing experience, which is canvas based,
|
|
75
|
+
// this should not happen
|
|
76
|
+
throw new Error("Selection rendering is not supported for SVG");
|
|
77
|
+
}
|
|
78
|
+
case "rectangle":
|
|
79
|
+
case "diamond":
|
|
80
|
+
case "ellipse": {
|
|
81
|
+
const shape = ShapeCache.generateElementShape(element, null);
|
|
82
|
+
const node = roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT);
|
|
83
|
+
if (opacity !== 1) {
|
|
84
|
+
node.setAttribute("stroke-opacity", `${opacity}`);
|
|
85
|
+
node.setAttribute("fill-opacity", `${opacity}`);
|
|
86
|
+
}
|
|
87
|
+
node.setAttribute("stroke-linecap", "round");
|
|
88
|
+
node.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
89
|
+
const g = maybeWrapNodesInFrameClipPath(element, root, [node], renderConfig.frameRendering, elementsMap);
|
|
90
|
+
addToRoot(g || node, element);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "iframe":
|
|
94
|
+
case "embeddable": {
|
|
95
|
+
// render placeholder rectangle
|
|
96
|
+
const shape = ShapeCache.generateElementShape(element, renderConfig);
|
|
97
|
+
const node = roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT);
|
|
98
|
+
const opacity = element.opacity / 100;
|
|
99
|
+
if (opacity !== 1) {
|
|
100
|
+
node.setAttribute("stroke-opacity", `${opacity}`);
|
|
101
|
+
node.setAttribute("fill-opacity", `${opacity}`);
|
|
102
|
+
}
|
|
103
|
+
node.setAttribute("stroke-linecap", "round");
|
|
104
|
+
node.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
105
|
+
addToRoot(node, element);
|
|
106
|
+
const label = createPlaceholderEmbeddableLabel(element);
|
|
107
|
+
renderElementToSvg(label, elementsMap, rsvg, root, files, label.x + offset.x - element.x, label.y + offset.y - element.y, renderConfig);
|
|
108
|
+
// render embeddable element + iframe
|
|
109
|
+
const embeddableNode = roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT);
|
|
110
|
+
embeddableNode.setAttribute("stroke-linecap", "round");
|
|
111
|
+
embeddableNode.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
112
|
+
while (embeddableNode.firstChild) {
|
|
113
|
+
embeddableNode.removeChild(embeddableNode.firstChild);
|
|
114
|
+
}
|
|
115
|
+
const radius = getCornerRadius(Math.min(element.width, element.height), element);
|
|
116
|
+
const embedLink = getEmbedLink(toValidURL(element.link || ""));
|
|
117
|
+
// if rendering embeddables explicitly disabled or
|
|
118
|
+
// embedding documents via srcdoc (which doesn't seem to work for SVGs)
|
|
119
|
+
// replace with a link instead
|
|
120
|
+
if (renderConfig.renderEmbeddables === false ||
|
|
121
|
+
embedLink?.type === "document") {
|
|
122
|
+
const anchorTag = svgRoot.ownerDocument.createElementNS(SVG_NS, "a");
|
|
123
|
+
anchorTag.setAttribute("href", normalizeLink(element.link || ""));
|
|
124
|
+
anchorTag.setAttribute("target", "_blank");
|
|
125
|
+
anchorTag.setAttribute("rel", "noopener noreferrer");
|
|
126
|
+
anchorTag.style.borderRadius = `${radius}px`;
|
|
127
|
+
embeddableNode.appendChild(anchorTag);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const foreignObject = svgRoot.ownerDocument.createElementNS(SVG_NS, "foreignObject");
|
|
131
|
+
foreignObject.style.width = `${element.width}px`;
|
|
132
|
+
foreignObject.style.height = `${element.height}px`;
|
|
133
|
+
foreignObject.style.border = "none";
|
|
134
|
+
const div = foreignObject.ownerDocument.createElementNS(SVG_NS, "div");
|
|
135
|
+
div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
136
|
+
div.style.width = "100%";
|
|
137
|
+
div.style.height = "100%";
|
|
138
|
+
const iframe = div.ownerDocument.createElement("iframe");
|
|
139
|
+
iframe.src = embedLink?.link ?? "";
|
|
140
|
+
iframe.style.width = "100%";
|
|
141
|
+
iframe.style.height = "100%";
|
|
142
|
+
iframe.style.border = "none";
|
|
143
|
+
iframe.style.borderRadius = `${radius}px`;
|
|
144
|
+
iframe.style.top = "0";
|
|
145
|
+
iframe.style.left = "0";
|
|
146
|
+
iframe.allowFullscreen = true;
|
|
147
|
+
div.appendChild(iframe);
|
|
148
|
+
foreignObject.appendChild(div);
|
|
149
|
+
embeddableNode.appendChild(foreignObject);
|
|
150
|
+
}
|
|
151
|
+
addToRoot(embeddableNode, element);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "line":
|
|
155
|
+
case "arrow": {
|
|
156
|
+
const boundText = getBoundTextElement(element, elementsMap);
|
|
157
|
+
const maskPath = svgRoot.ownerDocument.createElementNS(SVG_NS, "mask");
|
|
158
|
+
if (boundText) {
|
|
159
|
+
maskPath.setAttribute("id", `mask-${element.id}`);
|
|
160
|
+
const maskRectVisible = svgRoot.ownerDocument.createElementNS(SVG_NS, "rect");
|
|
161
|
+
offsetX = offsetX || 0;
|
|
162
|
+
offsetY = offsetY || 0;
|
|
163
|
+
maskRectVisible.setAttribute("x", "0");
|
|
164
|
+
maskRectVisible.setAttribute("y", "0");
|
|
165
|
+
maskRectVisible.setAttribute("fill", "#fff");
|
|
166
|
+
maskRectVisible.setAttribute("width", `${element.width + 100 + offsetX}`);
|
|
167
|
+
maskRectVisible.setAttribute("height", `${element.height + 100 + offsetY}`);
|
|
168
|
+
maskPath.appendChild(maskRectVisible);
|
|
169
|
+
const maskRectInvisible = svgRoot.ownerDocument.createElementNS(SVG_NS, "rect");
|
|
170
|
+
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(element, boundText, elementsMap);
|
|
171
|
+
const maskX = offsetX + boundTextCoords.x - element.x;
|
|
172
|
+
const maskY = offsetY + boundTextCoords.y - element.y;
|
|
173
|
+
maskRectInvisible.setAttribute("x", maskX.toString());
|
|
174
|
+
maskRectInvisible.setAttribute("y", maskY.toString());
|
|
175
|
+
maskRectInvisible.setAttribute("fill", "#000");
|
|
176
|
+
maskRectInvisible.setAttribute("width", `${boundText.width}`);
|
|
177
|
+
maskRectInvisible.setAttribute("height", `${boundText.height}`);
|
|
178
|
+
maskRectInvisible.setAttribute("opacity", "1");
|
|
179
|
+
maskPath.appendChild(maskRectInvisible);
|
|
180
|
+
}
|
|
181
|
+
const group = svgRoot.ownerDocument.createElementNS(SVG_NS, "g");
|
|
182
|
+
if (boundText) {
|
|
183
|
+
group.setAttribute("mask", `url(#mask-${element.id})`);
|
|
184
|
+
}
|
|
185
|
+
group.setAttribute("stroke-linecap", "round");
|
|
186
|
+
const shapes = ShapeCache.generateElementShape(element, renderConfig);
|
|
187
|
+
shapes.forEach((shape) => {
|
|
188
|
+
const node = roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT);
|
|
189
|
+
if (opacity !== 1) {
|
|
190
|
+
node.setAttribute("stroke-opacity", `${opacity}`);
|
|
191
|
+
node.setAttribute("fill-opacity", `${opacity}`);
|
|
192
|
+
}
|
|
193
|
+
node.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
194
|
+
if (element.type === "line" &&
|
|
195
|
+
isPathALoop(element.points) &&
|
|
196
|
+
element.backgroundColor !== "transparent") {
|
|
197
|
+
node.setAttribute("fill-rule", "evenodd");
|
|
198
|
+
}
|
|
199
|
+
group.appendChild(node);
|
|
200
|
+
});
|
|
201
|
+
const g = maybeWrapNodesInFrameClipPath(element, root, [group, maskPath], renderConfig.frameRendering, elementsMap);
|
|
202
|
+
if (g) {
|
|
203
|
+
addToRoot(g, element);
|
|
204
|
+
root.appendChild(g);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
addToRoot(group, element);
|
|
208
|
+
root.append(maskPath);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case "freedraw": {
|
|
213
|
+
const backgroundFillShape = ShapeCache.generateElementShape(element, renderConfig);
|
|
214
|
+
const node = backgroundFillShape
|
|
215
|
+
? roughSVGDrawWithPrecision(rsvg, backgroundFillShape, MAX_DECIMALS_FOR_SVG_EXPORT)
|
|
216
|
+
: svgRoot.ownerDocument.createElementNS(SVG_NS, "g");
|
|
217
|
+
if (opacity !== 1) {
|
|
218
|
+
node.setAttribute("stroke-opacity", `${opacity}`);
|
|
219
|
+
node.setAttribute("fill-opacity", `${opacity}`);
|
|
220
|
+
}
|
|
221
|
+
node.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
222
|
+
node.setAttribute("stroke", "none");
|
|
223
|
+
const path = svgRoot.ownerDocument.createElementNS(SVG_NS, "path");
|
|
224
|
+
path.setAttribute("fill", element.strokeColor);
|
|
225
|
+
path.setAttribute("d", getFreeDrawSvgPath(element));
|
|
226
|
+
node.appendChild(path);
|
|
227
|
+
const g = maybeWrapNodesInFrameClipPath(element, root, [node], renderConfig.frameRendering, elementsMap);
|
|
228
|
+
addToRoot(g || node, element);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "image": {
|
|
232
|
+
const width = Math.round(element.width);
|
|
233
|
+
const height = Math.round(element.height);
|
|
234
|
+
const fileData = isInitializedImageElement(element) && files[element.fileId];
|
|
235
|
+
if (fileData) {
|
|
236
|
+
const symbolId = `image-${fileData.id}`;
|
|
237
|
+
let symbol = svgRoot.querySelector(`#${symbolId}`);
|
|
238
|
+
if (!symbol) {
|
|
239
|
+
symbol = svgRoot.ownerDocument.createElementNS(SVG_NS, "symbol");
|
|
240
|
+
symbol.id = symbolId;
|
|
241
|
+
const image = svgRoot.ownerDocument.createElementNS(SVG_NS, "image");
|
|
242
|
+
image.setAttribute("width", "100%");
|
|
243
|
+
image.setAttribute("height", "100%");
|
|
244
|
+
image.setAttribute("href", fileData.dataURL);
|
|
245
|
+
symbol.appendChild(image);
|
|
246
|
+
root.prepend(symbol);
|
|
247
|
+
}
|
|
248
|
+
const use = svgRoot.ownerDocument.createElementNS(SVG_NS, "use");
|
|
249
|
+
use.setAttribute("href", `#${symbolId}`);
|
|
250
|
+
// in dark theme, revert the image color filter
|
|
251
|
+
if (renderConfig.exportWithDarkMode &&
|
|
252
|
+
fileData.mimeType !== MIME_TYPES.svg) {
|
|
253
|
+
use.setAttribute("filter", IMAGE_INVERT_FILTER);
|
|
254
|
+
}
|
|
255
|
+
use.setAttribute("width", `${width}`);
|
|
256
|
+
use.setAttribute("height", `${height}`);
|
|
257
|
+
use.setAttribute("opacity", `${opacity}`);
|
|
258
|
+
// We first apply `scale` transforms (horizontal/vertical mirroring)
|
|
259
|
+
// on the <use> element, then apply translation and rotation
|
|
260
|
+
// on the <g> element which wraps the <use>.
|
|
261
|
+
// Doing this separately is a quick hack to to work around compositing
|
|
262
|
+
// the transformations correctly (the transform-origin was not being
|
|
263
|
+
// applied correctly).
|
|
264
|
+
if (element.scale[0] !== 1 || element.scale[1] !== 1) {
|
|
265
|
+
const translateX = element.scale[0] !== 1 ? -width : 0;
|
|
266
|
+
const translateY = element.scale[1] !== 1 ? -height : 0;
|
|
267
|
+
use.setAttribute("transform", `scale(${element.scale[0]}, ${element.scale[1]}) translate(${translateX} ${translateY})`);
|
|
268
|
+
}
|
|
269
|
+
const g = svgRoot.ownerDocument.createElementNS(SVG_NS, "g");
|
|
270
|
+
g.appendChild(use);
|
|
271
|
+
g.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
272
|
+
if (element.roundness) {
|
|
273
|
+
const clipPath = svgRoot.ownerDocument.createElementNS(SVG_NS, "clipPath");
|
|
274
|
+
clipPath.id = `image-clipPath-${element.id}`;
|
|
275
|
+
const clipRect = svgRoot.ownerDocument.createElementNS(SVG_NS, "rect");
|
|
276
|
+
const radius = getCornerRadius(Math.min(element.width, element.height), element);
|
|
277
|
+
clipRect.setAttribute("width", `${element.width}`);
|
|
278
|
+
clipRect.setAttribute("height", `${element.height}`);
|
|
279
|
+
clipRect.setAttribute("rx", `${radius}`);
|
|
280
|
+
clipRect.setAttribute("ry", `${radius}`);
|
|
281
|
+
clipPath.appendChild(clipRect);
|
|
282
|
+
addToRoot(clipPath, element);
|
|
283
|
+
g.setAttributeNS(SVG_NS, "clip-path", `url(#${clipPath.id})`);
|
|
284
|
+
}
|
|
285
|
+
const clipG = maybeWrapNodesInFrameClipPath(element, root, [g], renderConfig.frameRendering, elementsMap);
|
|
286
|
+
addToRoot(clipG || g, element);
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
// frames are not rendered and only acts as a container
|
|
291
|
+
case "frame":
|
|
292
|
+
case "magicframe": {
|
|
293
|
+
if (renderConfig.frameRendering.enabled &&
|
|
294
|
+
renderConfig.frameRendering.outline) {
|
|
295
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
296
|
+
rect.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
297
|
+
rect.setAttribute("width", `${element.width}px`);
|
|
298
|
+
rect.setAttribute("height", `${element.height}px`);
|
|
299
|
+
// Rounded corners
|
|
300
|
+
rect.setAttribute("rx", FRAME_STYLE.radius.toString());
|
|
301
|
+
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
|
302
|
+
rect.setAttribute("fill", "none");
|
|
303
|
+
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
|
|
304
|
+
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
|
305
|
+
addToRoot(rect, element);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
default: {
|
|
310
|
+
if (isTextElement(element)) {
|
|
311
|
+
const node = svgRoot.ownerDocument.createElementNS(SVG_NS, "g");
|
|
312
|
+
if (opacity !== 1) {
|
|
313
|
+
node.setAttribute("stroke-opacity", `${opacity}`);
|
|
314
|
+
node.setAttribute("fill-opacity", `${opacity}`);
|
|
315
|
+
}
|
|
316
|
+
node.setAttribute("transform", `translate(${offsetX || 0} ${offsetY || 0}) rotate(${degree} ${cx} ${cy})`);
|
|
317
|
+
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
|
318
|
+
const lineHeightPx = getLineHeightInPx(element.fontSize, element.lineHeight);
|
|
319
|
+
const horizontalOffset = element.textAlign === "center"
|
|
320
|
+
? element.width / 2
|
|
321
|
+
: element.textAlign === "right"
|
|
322
|
+
? element.width
|
|
323
|
+
: 0;
|
|
324
|
+
const verticalOffset = getVerticalOffset(element.fontFamily, element.fontSize, lineHeightPx);
|
|
325
|
+
const direction = isRTL(element.text) ? "rtl" : "ltr";
|
|
326
|
+
const textAnchor = element.textAlign === "center"
|
|
327
|
+
? "middle"
|
|
328
|
+
: element.textAlign === "right" || direction === "rtl"
|
|
329
|
+
? "end"
|
|
330
|
+
: "start";
|
|
331
|
+
for (let i = 0; i < lines.length; i++) {
|
|
332
|
+
const text = svgRoot.ownerDocument.createElementNS(SVG_NS, "text");
|
|
333
|
+
text.textContent = lines[i];
|
|
334
|
+
text.setAttribute("x", `${horizontalOffset}`);
|
|
335
|
+
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
|
|
336
|
+
text.setAttribute("font-family", getFontFamilyString(element));
|
|
337
|
+
text.setAttribute("font-size", `${element.fontSize}px`);
|
|
338
|
+
text.setAttribute("fill", element.strokeColor);
|
|
339
|
+
text.setAttribute("text-anchor", textAnchor);
|
|
340
|
+
text.setAttribute("style", "white-space: pre;");
|
|
341
|
+
text.setAttribute("direction", direction);
|
|
342
|
+
text.setAttribute("dominant-baseline", "alphabetic");
|
|
343
|
+
node.appendChild(text);
|
|
344
|
+
}
|
|
345
|
+
const g = maybeWrapNodesInFrameClipPath(element, root, [node], renderConfig.frameRendering, elementsMap);
|
|
346
|
+
addToRoot(g || node, element);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// @ts-ignore
|
|
350
|
+
throw new Error(`Unimplemented type ${element.type}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
export const renderSceneToSvg = (elements, elementsMap, rsvg, svgRoot, files, renderConfig) => {
|
|
356
|
+
if (!svgRoot) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// render elements
|
|
360
|
+
elements
|
|
361
|
+
.filter((el) => !isIframeLikeElement(el))
|
|
362
|
+
.forEach((element) => {
|
|
363
|
+
if (!element.isDeleted) {
|
|
364
|
+
try {
|
|
365
|
+
renderElementToSvg(element, elementsMap, rsvg, svgRoot, files, element.x + renderConfig.offsetX, element.y + renderConfig.offsetY, renderConfig);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
console.error(error);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
// render embeddables on top
|
|
373
|
+
elements
|
|
374
|
+
.filter((el) => isIframeLikeElement(el))
|
|
375
|
+
.forEach((element) => {
|
|
376
|
+
if (!element.isDeleted) {
|
|
377
|
+
try {
|
|
378
|
+
renderElementToSvg(element, elementsMap, rsvg, svgRoot, files, element.x + renderConfig.offsetX, element.y + renderConfig.offsetY, renderConfig);
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
console.error(error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isTextElement, refreshTextDimensions } from "../element";
|
|
2
2
|
import { newElementWith } from "../element/mutateElement";
|
|
3
|
+
import { getContainerElement } from "../element/textElement";
|
|
3
4
|
import { isBoundToContainer } from "../element/typeChecks";
|
|
4
5
|
import { getFontString } from "../utils";
|
|
5
6
|
import { ShapeCache } from "./ShapeCache";
|
|
@@ -42,7 +43,7 @@ export class Fonts {
|
|
|
42
43
|
ShapeCache.delete(element);
|
|
43
44
|
didUpdate = true;
|
|
44
45
|
return newElementWith(element, {
|
|
45
|
-
...refreshTextDimensions(element),
|
|
46
|
+
...refreshTextDimensions(element, getContainerElement(element, this.scene.getNonDeletedElementsMap()), this.scene.getNonDeletedElementsMap()),
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
return element;
|
|
@@ -16,7 +16,7 @@ export declare class Renderer {
|
|
|
16
16
|
pendingImageElementId: AppState["pendingImageElementId"];
|
|
17
17
|
versionNonce: ReturnType<InstanceType<typeof Scene>["getVersionNonce"]>;
|
|
18
18
|
}) => {
|
|
19
|
-
|
|
19
|
+
elementsMap: Map<string, NonDeletedExcalidrawElement> & import("../utility-types").MakeBrand<"NonDeletedElementsMap"> & import("../utility-types").MakeBrand<"RenderableElementsMap">;
|
|
20
20
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
|
21
21
|
}) & {
|
|
22
22
|
clear: () => void;
|