summaryrefslogtreecommitdiffstats
path: root/examples/with-script-in-browser
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit225db4a7805befe009fe055fc2ef5daedd6c04f9 (patch)
treea5b0d073daabaadceb2f5c1b18640d785b5a9c71 /examples/with-script-in-browser
parent8ff10d2bf233608b027f8503cb9c7100c9ee3f16 (diff)
refactor: examples/
Diffstat (limited to 'examples/with-script-in-browser')
-rw-r--r--examples/with-script-in-browser/.codesandbox/Dockerfile5
-rw-r--r--examples/with-script-in-browser/.codesandbox/tasks.json35
-rw-r--r--examples/with-script-in-browser/.gitignore2
-rw-r--r--examples/with-script-in-browser/components/CustomFooter.tsx73
-rw-r--r--examples/with-script-in-browser/components/ExampleApp.scss92
-rw-r--r--examples/with-script-in-browser/components/ExampleApp.tsx961
-rw-r--r--examples/with-script-in-browser/components/MobileFooter.tsx28
-rw-r--r--examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss66
-rw-r--r--examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx31
-rw-r--r--examples/with-script-in-browser/index.html31
-rw-r--r--examples/with-script-in-browser/index.tsx28
-rw-r--r--examples/with-script-in-browser/initialData.tsx994
-rw-r--r--examples/with-script-in-browser/package.json21
-rw-r--r--examples/with-script-in-browser/public/images/doremon.pngbin0 -> 201946 bytes
-rw-r--r--examples/with-script-in-browser/public/images/excalibot.pngbin0 -> 30330 bytes
-rw-r--r--examples/with-script-in-browser/public/images/pika.jpegbin0 -> 6250 bytes
-rw-r--r--examples/with-script-in-browser/public/images/rocket.jpegbin0 -> 40368 bytes
-rw-r--r--examples/with-script-in-browser/tsconfig.json9
-rw-r--r--examples/with-script-in-browser/utils.ts145
-rw-r--r--examples/with-script-in-browser/vercel.json5
-rw-r--r--examples/with-script-in-browser/vite.config.mts19
21 files changed, 2545 insertions, 0 deletions
diff --git a/examples/with-script-in-browser/.codesandbox/Dockerfile b/examples/with-script-in-browser/.codesandbox/Dockerfile
new file mode 100644
index 0000000..fd5b38d
--- /dev/null
+++ b/examples/with-script-in-browser/.codesandbox/Dockerfile
@@ -0,0 +1,5 @@
+FROM node:18-bullseye
+
+# Vite wants to open the browser using `open`, so we
+# need to install those utils.
+RUN apt update -y && apt install -y xdg-utils
diff --git a/examples/with-script-in-browser/.codesandbox/tasks.json b/examples/with-script-in-browser/.codesandbox/tasks.json
new file mode 100644
index 0000000..990c21a
--- /dev/null
+++ b/examples/with-script-in-browser/.codesandbox/tasks.json
@@ -0,0 +1,35 @@
+{
+ // These tasks will run in order when initializing your CodeSandbox project.
+ "setupTasks": [
+ {
+ "name": "Install Dependencies",
+ "command": "yarn install"
+ }
+ ],
+
+ // These tasks can be run from CodeSandbox. Running one will open a log in the app.
+ "tasks": {
+ "build": {
+ "name": "Build",
+ "command": "yarn build",
+ "runAtStart": false
+ },
+ "start": {
+ "name": "Start Example",
+ "command": "yarn start",
+ "runAtStart": true,
+ "preview": {
+ "port": 3001
+ }
+ },
+ "install-deps": {
+ "name": "Install Dependencies",
+ "command": "yarn install",
+ "restartOn": {
+ "files": ["yarn.lock"],
+ "branch": false,
+ "resume": false
+ }
+ }
+ }
+}
diff --git a/examples/with-script-in-browser/.gitignore b/examples/with-script-in-browser/.gitignore
new file mode 100644
index 0000000..f1b2f5e
--- /dev/null
+++ b/examples/with-script-in-browser/.gitignore
@@ -0,0 +1,2 @@
+# copied assets
+public/**/*.woff2 \ No newline at end of file
diff --git a/examples/with-script-in-browser/components/CustomFooter.tsx b/examples/with-script-in-browser/components/CustomFooter.tsx
new file mode 100644
index 0000000..72fd199
--- /dev/null
+++ b/examples/with-script-in-browser/components/CustomFooter.tsx
@@ -0,0 +1,73 @@
+import React from "react";
+import type * as TExcalidraw from "@excalidraw/excalidraw";
+import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
+
+const COMMENT_SVG = (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="feather feather-message-circle"
+ >
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
+ </svg>
+);
+
+const CustomFooter = ({
+ excalidrawAPI,
+ excalidrawLib,
+}: {
+ excalidrawAPI: ExcalidrawImperativeAPI;
+ excalidrawLib: typeof TExcalidraw;
+}) => {
+ const { Button, MIME_TYPES } = excalidrawLib;
+
+ return (
+ <>
+ <Button
+ onSelect={() => alert("General Kenobi!")}
+ style={{ marginLeft: "1rem", width: "auto" }}
+ title="Hello there!"
+ >
+ Hit me
+ </Button>
+ <Button
+ className="custom-element"
+ onSelect={() => {
+ excalidrawAPI?.setActiveTool({
+ type: "custom",
+ customType: "comment",
+ });
+ const url = `data:${MIME_TYPES.svg},${encodeURIComponent(
+ `<svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ class="feather feather-message-circle"
+ >
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
+ </svg>`,
+ )}`;
+ excalidrawAPI?.setCursor(`url(${url}), auto`);
+ }}
+ title="Comments!"
+ >
+ {COMMENT_SVG}
+ </Button>
+ </>
+ );
+};
+
+export default CustomFooter;
diff --git a/examples/with-script-in-browser/components/ExampleApp.scss b/examples/with-script-in-browser/components/ExampleApp.scss
new file mode 100644
index 0000000..e41a77c
--- /dev/null
+++ b/examples/with-script-in-browser/components/ExampleApp.scss
@@ -0,0 +1,92 @@
+.App {
+ font-family: sans-serif;
+ text-align: center;
+
+ .comment-avatar {
+ background: #faa2c1;
+ border-radius: 66px 67px 67px 0px;
+ width: 2rem;
+ height: 2rem;
+ padding: 4px;
+ margin: 4px;
+ img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ }
+ }
+ .app-title {
+ margin-block-start: 0.83em;
+ margin-block-end: 0.83em;
+ }
+}
+
+.button-wrapper {
+ input[type="checkbox"] {
+ margin: 5px;
+ }
+ button {
+ z-index: 1;
+ height: 40px;
+ max-width: 200px;
+ margin: 10px;
+ padding: 5px;
+ }
+}
+
+.excalidraw .App-menu_top .buttonList {
+ display: flex;
+}
+
+.excalidraw-wrapper {
+ height: 800px;
+ margin: 50px;
+ position: relative;
+ overflow: hidden;
+}
+
+:root[dir="ltr"]
+ .excalidraw
+ .layer-ui__wrapper
+ .zen-mode-transition.App-menu_bottom--transition-left {
+ transform: none;
+}
+
+.excalidraw .panelColumn {
+ text-align: left;
+}
+
+.export-wrapper {
+ display: flex;
+ flex-direction: column;
+ margin: 50px;
+
+ &__checkbox {
+ display: flex;
+ }
+}
+
+.excalidraw {
+ --color-primary: #faa2c1;
+ --color-primary-darker: #f783ac;
+ --color-primary-darkest: #e64980;
+ --color-primary-light: #fcc2d7;
+
+ button.custom-element {
+ width: 2rem;
+ height: 2rem;
+ }
+
+ .custom-footer,
+ .custom-element {
+ padding: 0.1rem;
+ margin: 0 8px;
+ }
+ .layer-ui__wrapper__footer.App-menu_bottom {
+ align-items: stretch;
+ }
+ // till its merged in OSS
+ .App-toolbar-container .mobile-misc-tools-container {
+ position: absolute;
+ }
+}
diff --git a/examples/with-script-in-browser/components/ExampleApp.tsx b/examples/with-script-in-browser/components/ExampleApp.tsx
new file mode 100644
index 0000000..08c8032
--- /dev/null
+++ b/examples/with-script-in-browser/components/ExampleApp.tsx
@@ -0,0 +1,961 @@
+import React, {
+ useEffect,
+ useState,
+ useRef,
+ useCallback,
+ Children,
+ cloneElement,
+} from "react";
+import ExampleSidebar from "./sidebar/ExampleSidebar";
+
+import type * as TExcalidraw from "@excalidraw/excalidraw";
+
+import { nanoid } from "nanoid";
+
+import type { ResolvablePromise } from "../utils";
+import {
+ resolvablePromise,
+ distance2d,
+ fileOpen,
+ withBatchedUpdates,
+ withBatchedUpdatesThrottled,
+} from "../utils";
+
+import CustomFooter from "./CustomFooter";
+import MobileFooter from "./MobileFooter";
+import initialData from "../initialData";
+
+import type {
+ AppState,
+ BinaryFileData,
+ ExcalidrawImperativeAPI,
+ ExcalidrawInitialDataState,
+ Gesture,
+ LibraryItems,
+ PointerDownState as ExcalidrawPointerDownState,
+} from "@excalidraw/excalidraw/types";
+import type {
+ NonDeletedExcalidrawElement,
+ Theme,
+} from "@excalidraw/excalidraw/element/types";
+import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
+
+import "./ExampleApp.scss";
+
+type Comment = {
+ x: number;
+ y: number;
+ value: string;
+ id?: string;
+};
+
+type PointerDownState = {
+ x: number;
+ y: number;
+ hitElement: Comment;
+ onMove: any;
+ onUp: any;
+ hitElementOffsets: {
+ x: number;
+ y: number;
+ };
+};
+
+const COMMENT_ICON_DIMENSION = 32;
+const COMMENT_INPUT_HEIGHT = 50;
+const COMMENT_INPUT_WIDTH = 150;
+
+export interface AppProps {
+ appTitle: string;
+ useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
+ customArgs?: any[];
+ children: React.ReactNode;
+ excalidrawLib: typeof TExcalidraw;
+}
+
+export default function ExampleApp({
+ appTitle,
+ useCustom,
+ customArgs,
+ children,
+ excalidrawLib,
+}: AppProps) {
+ const {
+ exportToCanvas,
+ exportToSvg,
+ exportToBlob,
+ exportToClipboard,
+ useHandleLibrary,
+ MIME_TYPES,
+ sceneCoordsToViewportCoords,
+ viewportCoordsToSceneCoords,
+ restoreElements,
+ Sidebar,
+ Footer,
+ WelcomeScreen,
+ MainMenu,
+ LiveCollaborationTrigger,
+ convertToExcalidrawElements,
+ TTDDialog,
+ TTDDialogTrigger,
+ ROUNDNESS,
+ loadSceneOrLibraryFromBlob,
+ } = excalidrawLib;
+ const appRef = useRef<any>(null);
+ const [viewModeEnabled, setViewModeEnabled] = useState(false);
+ const [zenModeEnabled, setZenModeEnabled] = useState(false);
+ const [gridModeEnabled, setGridModeEnabled] = useState(false);
+ const [blobUrl, setBlobUrl] = useState<string>("");
+ const [canvasUrl, setCanvasUrl] = useState<string>("");
+ const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
+ const [exportEmbedScene, setExportEmbedScene] = useState(false);
+ const [theme, setTheme] = useState<Theme>("light");
+ const [disableImageTool, setDisableImageTool] = useState(false);
+ const [isCollaborating, setIsCollaborating] = useState(false);
+ const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
+ {},
+ );
+ const [comment, setComment] = useState<Comment | null>(null);
+
+ const initialStatePromiseRef = useRef<{
+ promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
+ }>({ promise: null! });
+ if (!initialStatePromiseRef.current.promise) {
+ initialStatePromiseRef.current.promise =
+ resolvablePromise<ExcalidrawInitialDataState | null>();
+ }
+
+ const [excalidrawAPI, setExcalidrawAPI] =
+ useState<ExcalidrawImperativeAPI | null>(null);
+
+ useCustom(excalidrawAPI, customArgs);
+
+ useHandleLibrary({ excalidrawAPI });
+
+ useEffect(() => {
+ if (!excalidrawAPI) {
+ return;
+ }
+ const fetchData = async () => {
+ const res = await fetch("/images/rocket.jpeg");
+ const imageData = await res.blob();
+ const reader = new FileReader();
+ reader.readAsDataURL(imageData);
+
+ reader.onload = function () {
+ const imagesArray: BinaryFileData[] = [
+ {
+ id: "rocket" as BinaryFileData["id"],
+ dataURL: reader.result as BinaryFileData["dataURL"],
+ mimeType: MIME_TYPES.jpg,
+ created: 1644915140367,
+ lastRetrieved: 1644915140367,
+ },
+ ];
+
+ //@ts-ignore
+ initialStatePromiseRef.current.promise.resolve({
+ ...initialData,
+ elements: convertToExcalidrawElements(initialData.elements),
+ });
+ excalidrawAPI.addFiles(imagesArray);
+ };
+ };
+ fetchData();
+ }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]);
+
+ const renderExcalidraw = (children: React.ReactNode) => {
+ const Excalidraw: any = Children.toArray(children).find(
+ (child) =>
+ React.isValidElement(child) &&
+ typeof child.type !== "string" &&
+ //@ts-ignore
+ child.type.displayName === "Excalidraw",
+ );
+ if (!Excalidraw) {
+ return;
+ }
+ const newElement = cloneElement(
+ Excalidraw,
+ {
+ excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
+ initialData: initialStatePromiseRef.current.promise,
+ onChange: (
+ elements: NonDeletedExcalidrawElement[],
+ state: AppState,
+ ) => {
+ console.info("Elements :", elements, "State : ", state);
+ },
+ onPointerUpdate: (payload: {
+ pointer: { x: number; y: number };
+ button: "down" | "up";
+ pointersMap: Gesture["pointers"];
+ }) => setPointerData(payload),
+ viewModeEnabled,
+ zenModeEnabled,
+ gridModeEnabled,
+ theme,
+ name: "Custom name of drawing",
+ UIOptions: {
+ canvasActions: {
+ loadScene: false,
+ },
+ tools: { image: !disableImageTool },
+ },
+ renderTopRightUI,
+ onLinkOpen,
+ onPointerDown,
+ onScrollChange: rerenderCommentIcons,
+ validateEmbeddable: true,
+ },
+ <>
+ {excalidrawAPI && (
+ <Footer>
+ <CustomFooter
+ excalidrawAPI={excalidrawAPI}
+ excalidrawLib={excalidrawLib}
+ />
+ </Footer>
+ )}
+ <WelcomeScreen />
+ <Sidebar name="custom">
+ <Sidebar.Tabs>
+ <Sidebar.Header />
+ <Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
+ <Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
+ <Sidebar.TabTriggers>
+ <Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
+ <Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
+ </Sidebar.TabTriggers>
+ </Sidebar.Tabs>
+ </Sidebar>
+ <Sidebar.Trigger
+ name="custom"
+ tab="one"
+ style={{
+ position: "absolute",
+ left: "50%",
+ transform: "translateX(-50%)",
+ bottom: "20px",
+ zIndex: 9999999999999999,
+ }}
+ >
+ Toggle Custom Sidebar
+ </Sidebar.Trigger>
+ {renderMenu()}
+ {excalidrawAPI && (
+ <TTDDialogTrigger icon={<span>😀</span>}>
+ Text to diagram
+ </TTDDialogTrigger>
+ )}
+ <TTDDialog
+ onTextSubmit={async (_) => {
+ console.info("submit");
+ // sleep for 2s
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ throw new Error("error, go away now");
+ // return "dummy";
+ }}
+ />
+ </>,
+ );
+ return newElement;
+ };
+ const renderTopRightUI = (isMobile: boolean) => {
+ return (
+ <>
+ {!isMobile && (
+ <LiveCollaborationTrigger
+ isCollaborating={isCollaborating}
+ onSelect={() => {
+ window.alert("Collab dialog clicked");
+ }}
+ />
+ )}
+ <button
+ onClick={() => alert("This is an empty top right UI")}
+ style={{ height: "2.5rem" }}
+ >
+ Click me
+ </button>
+ </>
+ );
+ };
+
+ const loadSceneOrLibrary = async () => {
+ const file = await fileOpen({ description: "Excalidraw or library file" });
+ const contents = await loadSceneOrLibraryFromBlob(file, null, null);
+ if (contents.type === MIME_TYPES.excalidraw) {
+ excalidrawAPI?.updateScene(contents.data as any);
+ } else if (contents.type === MIME_TYPES.excalidrawlib) {
+ excalidrawAPI?.updateLibrary({
+ libraryItems: (contents.data as ImportedLibraryData).libraryItems!,
+ openLibraryMenu: true,
+ });
+ }
+ };
+
+ const updateScene = () => {
+ const sceneData = {
+ elements: restoreElements(
+ convertToExcalidrawElements([
+ {
+ type: "rectangle",
+ id: "rect-1",
+ fillStyle: "hachure",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ angle: 0,
+ x: 100.50390625,
+ y: 93.67578125,
+ strokeColor: "#c92a2a",
+ width: 186.47265625,
+ height: 141.9765625,
+ seed: 1968410350,
+ roundness: {
+ type: ROUNDNESS.ADAPTIVE_RADIUS,
+ value: 32,
+ },
+ },
+ {
+ type: "arrow",
+ x: 300,
+ y: 150,
+ start: { id: "rect-1" },
+ end: { type: "ellipse" },
+ },
+ {
+ type: "text",
+ x: 300,
+ y: 100,
+ text: "HELLO WORLD!",
+ },
+ ]),
+ null,
+ ),
+ appState: {
+ viewBackgroundColor: "#edf2ff",
+ },
+ };
+ excalidrawAPI?.updateScene(sceneData);
+ };
+
+ const onLinkOpen = useCallback(
+ (
+ element: NonDeletedExcalidrawElement,
+ event: CustomEvent<{
+ nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
+ }>,
+ ) => {
+ const link = element.link!;
+ const { nativeEvent } = event.detail;
+ const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
+ const isNewWindow = nativeEvent.shiftKey;
+ const isInternalLink =
+ link.startsWith("/") || link.includes(window.location.origin);
+ if (isInternalLink && !isNewTab && !isNewWindow) {
+ // signal that we're handling the redirect ourselves
+ event.preventDefault();
+ // do a custom redirect, such as passing to react-router
+ // ...
+ }
+ },
+ [],
+ );
+
+ const onCopy = async (type: "png" | "svg" | "json") => {
+ if (!excalidrawAPI) {
+ return false;
+ }
+ await exportToClipboard({
+ elements: excalidrawAPI.getSceneElements(),
+ appState: excalidrawAPI.getAppState(),
+ files: excalidrawAPI.getFiles(),
+ type,
+ });
+ window.alert(`Copied to clipboard as ${type} successfully`);
+ };
+
+ const [pointerData, setPointerData] = useState<{
+ pointer: { x: number; y: number };
+ button: "down" | "up";
+ pointersMap: Gesture["pointers"];
+ } | null>(null);
+
+ const onPointerDown = (
+ activeTool: AppState["activeTool"],
+ pointerDownState: ExcalidrawPointerDownState,
+ ) => {
+ if (activeTool.type === "custom" && activeTool.customType === "comment") {
+ const { x, y } = pointerDownState.origin;
+ setComment({ x, y, value: "" });
+ }
+ };
+
+ const rerenderCommentIcons = () => {
+ if (!excalidrawAPI) {
+ return false;
+ }
+ const commentIconsElements = appRef.current.querySelectorAll(
+ ".comment-icon",
+ ) as HTMLElement[];
+ commentIconsElements.forEach((ele) => {
+ const id = ele.id;
+ const appstate = excalidrawAPI.getAppState();
+ const { x, y } = sceneCoordsToViewportCoords(
+ { sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
+ appstate,
+ );
+ ele.style.left = `${
+ x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft
+ }px`;
+ ele.style.top = `${
+ y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop
+ }px`;
+ });
+ };
+
+ const onPointerMoveFromPointerDownHandler = (
+ pointerDownState: PointerDownState,
+ ) => {
+ return withBatchedUpdatesThrottled((event) => {
+ if (!excalidrawAPI) {
+ return false;
+ }
+ const { x, y } = viewportCoordsToSceneCoords(
+ {
+ clientX: event.clientX - pointerDownState.hitElementOffsets.x,
+ clientY: event.clientY - pointerDownState.hitElementOffsets.y,
+ },
+ excalidrawAPI.getAppState(),
+ );
+ setCommentIcons({
+ ...commentIcons,
+ [pointerDownState.hitElement.id!]: {
+ ...commentIcons[pointerDownState.hitElement.id!],
+ x,
+ y,
+ },
+ });
+ });
+ };
+ const onPointerUpFromPointerDownHandler = (
+ pointerDownState: PointerDownState,
+ ) => {
+ return withBatchedUpdates((event) => {
+ window.removeEventListener("pointermove", pointerDownState.onMove);
+ window.removeEventListener("pointerup", pointerDownState.onUp);
+ excalidrawAPI?.setActiveTool({ type: "selection" });
+ const distance = distance2d(
+ pointerDownState.x,
+ pointerDownState.y,
+ event.clientX,
+ event.clientY,
+ );
+ if (distance === 0) {
+ if (!comment) {
+ setComment({
+ x: pointerDownState.hitElement.x + 60,
+ y: pointerDownState.hitElement.y,
+ value: pointerDownState.hitElement.value,
+ id: pointerDownState.hitElement.id,
+ });
+ } else {
+ setComment(null);
+ }
+ }
+ });
+ };
+
+ const renderCommentIcons = () => {
+ return Object.values(commentIcons).map((commentIcon) => {
+ if (!excalidrawAPI) {
+ return false;
+ }
+ const appState = excalidrawAPI.getAppState();
+ const { x, y } = sceneCoordsToViewportCoords(
+ { sceneX: commentIcon.x, sceneY: commentIcon.y },
+ excalidrawAPI.getAppState(),
+ );
+ return (
+ <div
+ id={commentIcon.id}
+ key={commentIcon.id}
+ style={{
+ top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`,
+ left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`,
+ position: "absolute",
+ zIndex: 1,
+ width: `${COMMENT_ICON_DIMENSION}px`,
+ height: `${COMMENT_ICON_DIMENSION}px`,
+ cursor: "pointer",
+ touchAction: "none",
+ }}
+ className="comment-icon"
+ onPointerDown={(event) => {
+ event.preventDefault();
+ if (comment) {
+ commentIcon.value = comment.value;
+ saveComment();
+ }
+ const pointerDownState: any = {
+ x: event.clientX,
+ y: event.clientY,
+ hitElement: commentIcon,
+ hitElementOffsets: { x: event.clientX - x, y: event.clientY - y },
+ };
+ const onPointerMove =
+ onPointerMoveFromPointerDownHandler(pointerDownState);
+ const onPointerUp =
+ onPointerUpFromPointerDownHandler(pointerDownState);
+ window.addEventListener("pointermove", onPointerMove);
+ window.addEventListener("pointerup", onPointerUp);
+
+ pointerDownState.onMove = onPointerMove;
+ pointerDownState.onUp = onPointerUp;
+
+ excalidrawAPI?.setActiveTool({
+ type: "custom",
+ customType: "comment",
+ });
+ }}
+ >
+ <div className="comment-avatar">
+ <img src="images/doremon.png" alt="doremon" />
+ </div>
+ </div>
+ );
+ });
+ };
+
+ const saveComment = () => {
+ if (!comment) {
+ return;
+ }
+ if (!comment.id && !comment.value) {
+ setComment(null);
+ return;
+ }
+ const id = comment.id || nanoid();
+ setCommentIcons({
+ ...commentIcons,
+ [id]: {
+ x: comment.id ? comment.x - 60 : comment.x,
+ y: comment.y,
+ id,
+ value: comment.value,
+ },
+ });
+ setComment(null);
+ };
+
+ const renderComment = () => {
+ if (!comment) {
+ return null;
+ }
+ const appState = excalidrawAPI?.getAppState()!;
+ const { x, y } = sceneCoordsToViewportCoords(
+ { sceneX: comment.x, sceneY: comment.y },
+ appState,
+ );
+ let top = y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop;
+ let left = x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft;
+
+ if (
+ top + COMMENT_INPUT_HEIGHT <
+ appState.offsetTop + COMMENT_INPUT_HEIGHT
+ ) {
+ top = COMMENT_ICON_DIMENSION / 2;
+ }
+ if (top + COMMENT_INPUT_HEIGHT > appState.height) {
+ top = appState.height - COMMENT_INPUT_HEIGHT - COMMENT_ICON_DIMENSION / 2;
+ }
+ if (
+ left + COMMENT_INPUT_WIDTH <
+ appState.offsetLeft + COMMENT_INPUT_WIDTH
+ ) {
+ left = COMMENT_ICON_DIMENSION / 2;
+ }
+ if (left + COMMENT_INPUT_WIDTH > appState.width) {
+ left = appState.width - COMMENT_INPUT_WIDTH - COMMENT_ICON_DIMENSION / 2;
+ }
+
+ return (
+ <textarea
+ className="comment"
+ style={{
+ top: `${top}px`,
+ left: `${left}px`,
+ position: "absolute",
+ zIndex: 1,
+ height: `${COMMENT_INPUT_HEIGHT}px`,
+ width: `${COMMENT_INPUT_WIDTH}px`,
+ }}
+ ref={(ref) => {
+ setTimeout(() => ref?.focus());
+ }}
+ placeholder={comment.value ? "Reply" : "Comment"}
+ value={comment.value}
+ onChange={(event) => {
+ setComment({ ...comment, value: event.target.value });
+ }}
+ onBlur={saveComment}
+ onKeyDown={(event) => {
+ if (!event.shiftKey && event.key === "Enter") {
+ event.preventDefault();
+ saveComment();
+ }
+ }}
+ />
+ );
+ };
+
+ const renderMenu = () => {
+ return (
+ <MainMenu>
+ <MainMenu.DefaultItems.SaveAsImage />
+ <MainMenu.DefaultItems.Export />
+ <MainMenu.Separator />
+ <MainMenu.DefaultItems.LiveCollaborationTrigger
+ isCollaborating={isCollaborating}
+ onSelect={() => window.alert("You clicked on collab button")}
+ />
+ <MainMenu.Group title="Excalidraw links">
+ <MainMenu.DefaultItems.Socials />
+ </MainMenu.Group>
+ <MainMenu.Separator />
+ <MainMenu.ItemCustom>
+ <button
+ style={{ height: "2rem" }}
+ onClick={() => window.alert("custom menu item")}
+ >
+ custom item
+ </button>
+ </MainMenu.ItemCustom>
+ <MainMenu.DefaultItems.Help />
+
+ {excalidrawAPI && (
+ <MobileFooter
+ excalidrawLib={excalidrawLib}
+ excalidrawAPI={excalidrawAPI}
+ />
+ )}
+ </MainMenu>
+ );
+ };
+
+ return (
+ <div className="App" ref={appRef}>
+ <h1>{appTitle}</h1>
+ {/* TODO fix type */}
+ <ExampleSidebar>
+ <div className="button-wrapper">
+ <button onClick={loadSceneOrLibrary}>Load Scene or Library</button>
+ <button className="update-scene" onClick={updateScene}>
+ Update Scene
+ </button>
+ <button
+ className="reset-scene"
+ onClick={() => {
+ excalidrawAPI?.resetScene();
+ }}
+ >
+ Reset Scene
+ </button>
+ <button
+ onClick={() => {
+ const libraryItems: LibraryItems = [
+ {
+ status: "published",
+ id: "1",
+ created: 1,
+ elements: initialData.libraryItems[1] as any,
+ },
+ {
+ status: "unpublished",
+ id: "2",
+ created: 2,
+ elements: initialData.libraryItems[1] as any,
+ },
+ ];
+ excalidrawAPI?.updateLibrary({
+ libraryItems,
+ });
+ }}
+ >
+ Update Library
+ </button>
+
+ <label>
+ <input
+ type="checkbox"
+ checked={viewModeEnabled}
+ onChange={() => setViewModeEnabled(!viewModeEnabled)}
+ />
+ View mode
+ </label>
+ <label>
+ <input
+ type="checkbox"
+ checked={zenModeEnabled}
+ onChange={() => setZenModeEnabled(!zenModeEnabled)}
+ />
+ Zen mode
+ </label>
+ <label>
+ <input
+ type="checkbox"
+ checked={gridModeEnabled}
+ onChange={() => setGridModeEnabled(!gridModeEnabled)}
+ />
+ Grid mode
+ </label>
+ <label>
+ <input
+ type="checkbox"
+ checked={theme === "dark"}
+ onChange={() => {
+ setTheme(theme === "light" ? "dark" : "light");
+ }}
+ />
+ Switch to Dark Theme
+ </label>
+ <label>
+ <input
+ type="checkbox"
+ checked={disableImageTool === true}
+ onChange={() => {
+ setDisableImageTool(!disableImageTool);
+ }}
+ />
+ Disable Image Tool
+ </label>
+ <label>
+ <input
+ type="checkbox"
+ checked={isCollaborating}
+ onChange={() => {
+ if (!isCollaborating) {
+ const collaborators = new Map();
+ collaborators.set("id1", {
+ username: "Doremon",
+ avatarUrl: "images/doremon.png",
+ });
+ collaborators.set("id2", {
+ username: "Excalibot",
+ avatarUrl: "images/excalibot.png",
+ });
+ collaborators.set("id3", {
+ username: "Pika",
+ avatarUrl: "images/pika.jpeg",
+ });
+ collaborators.set("id4", {
+ username: "fallback",
+ avatarUrl: "https://example.com",
+ });
+ excalidrawAPI?.updateScene({ collaborators });
+ } else {
+ excalidrawAPI?.updateScene({
+ collaborators: new Map(),
+ });
+ }
+ setIsCollaborating(!isCollaborating);
+ }}
+ />
+ Show collaborators
+ </label>
+ <div>
+ <button onClick={onCopy.bind(null, "png")}>
+ Copy to Clipboard as PNG
+ </button>
+ <button onClick={onCopy.bind(null, "svg")}>
+ Copy to Clipboard as SVG
+ </button>
+ <button onClick={onCopy.bind(null, "json")}>
+ Copy to Clipboard as JSON
+ </button>
+ </div>
+ <div
+ style={{
+ display: "flex",
+ gap: "1em",
+ justifyContent: "center",
+ marginTop: "1em",
+ }}
+ >
+ <div>x: {pointerData?.pointer.x ?? 0}</div>
+ <div>y: {pointerData?.pointer.y ?? 0}</div>
+ </div>
+ </div>
+ <div className="excalidraw-wrapper">
+ {renderExcalidraw(children)}
+ {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
+ {comment && renderComment()}
+ </div>
+
+ <div className="export-wrapper button-wrapper">
+ <label className="export-wrapper__checkbox">
+ <input
+ type="checkbox"
+ checked={exportWithDarkMode}
+ onChange={() => setExportWithDarkMode(!exportWithDarkMode)}
+ />
+ Export with dark mode
+ </label>
+ <label className="export-wrapper__checkbox">
+ <input
+ type="checkbox"
+ checked={exportEmbedScene}
+ onChange={() => setExportEmbedScene(!exportEmbedScene)}
+ />
+ Export with embed scene
+ </label>
+ <button
+ onClick={async () => {
+ if (!excalidrawAPI) {
+ return;
+ }
+ const svg = await exportToSvg({
+ elements: excalidrawAPI?.getSceneElements(),
+ appState: {
+ ...initialData.appState,
+ exportWithDarkMode,
+ exportEmbedScene,
+ width: 300,
+ height: 100,
+ },
+ files: excalidrawAPI?.getFiles(),
+ });
+ appRef.current.querySelector(".export-svg").innerHTML =
+ svg.outerHTML;
+ }}
+ >
+ Export to SVG
+ </button>
+ <div className="export export-svg"></div>
+
+ <button
+ onClick={async () => {
+ if (!excalidrawAPI) {
+ return;
+ }
+ const blob = await exportToBlob({
+ elements: excalidrawAPI?.getSceneElements(),
+ mimeType: "image/png",
+ appState: {
+ ...initialData.appState,
+ exportEmbedScene,
+ exportWithDarkMode,
+ },
+ files: excalidrawAPI?.getFiles(),
+ });
+ setBlobUrl(window.URL.createObjectURL(blob));
+ }}
+ >
+ Export to Blob
+ </button>
+ <div className="export export-blob">
+ <img src={blobUrl} alt="" />
+ </div>
+ <button
+ onClick={async () => {
+ if (!excalidrawAPI) {
+ return;
+ }
+ const canvas = await exportToCanvas({
+ elements: excalidrawAPI.getSceneElements(),
+ appState: {
+ ...initialData.appState,
+ exportWithDarkMode,
+ },
+ files: excalidrawAPI.getFiles(),
+ });
+ const ctx = canvas.getContext("2d")!;
+ ctx.font = "30px Excalifont";
+ ctx.strokeText("My custom text", 50, 60);
+ setCanvasUrl(canvas.toDataURL());
+ }}
+ >
+ Export to Canvas
+ </button>
+ <button
+ onClick={async () => {
+ if (!excalidrawAPI) {
+ return;
+ }
+ const canvas = await exportToCanvas({
+ elements: excalidrawAPI.getSceneElements(),
+ appState: {
+ ...initialData.appState,
+ exportWithDarkMode,
+ },
+ files: excalidrawAPI.getFiles(),
+ });
+ const ctx = canvas.getContext("2d")!;
+ ctx.font = "30px Excalifont";
+ ctx.strokeText("My custom text", 50, 60);
+ setCanvasUrl(canvas.toDataURL());
+ }}
+ >
+ Export to Canvas
+ </button>
+ <button
+ type="button"
+ onClick={() => {
+ if (!excalidrawAPI) {
+ return;
+ }
+
+ const elements = excalidrawAPI.getSceneElements();
+ excalidrawAPI.scrollToContent(elements[0], {
+ fitToViewport: true,
+ });
+ }}
+ >
+ Fit to viewport, first element
+ </button>
+ <button
+ type="button"
+ onClick={() => {
+ if (!excalidrawAPI) {
+ return;
+ }
+
+ const elements = excalidrawAPI.getSceneElements();
+ excalidrawAPI.scrollToContent(elements[0], {
+ fitToContent: true,
+ });
+
+ excalidrawAPI.scrollToContent(elements[0], {
+ fitToContent: true,
+ });
+ }}
+ >
+ Fit to content, first element
+ </button>
+ <button
+ type="button"
+ onClick={() => {
+ if (!excalidrawAPI) {
+ return;
+ }
+
+ const elements = excalidrawAPI.getSceneElements();
+ excalidrawAPI.scrollToContent(elements[0], {
+ fitToContent: true,
+ });
+
+ excalidrawAPI.scrollToContent(elements[0]);
+ }}
+ >
+ Scroll to first element, no fitToContent, no fitToViewport
+ </button>
+ <div className="export export-canvas">
+ <img src={canvasUrl} alt="" />
+ </div>
+ </div>
+ </ExampleSidebar>
+ </div>
+ );
+}
diff --git a/examples/with-script-in-browser/components/MobileFooter.tsx b/examples/with-script-in-browser/components/MobileFooter.tsx
new file mode 100644
index 0000000..c8fc0f1
--- /dev/null
+++ b/examples/with-script-in-browser/components/MobileFooter.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
+import CustomFooter from "./CustomFooter";
+import type * as TExcalidraw from "@excalidraw/excalidraw";
+
+const MobileFooter = ({
+ excalidrawAPI,
+ excalidrawLib,
+}: {
+ excalidrawAPI: ExcalidrawImperativeAPI;
+ excalidrawLib: typeof TExcalidraw;
+}) => {
+ const { useDevice, Footer } = excalidrawLib;
+
+ const device = useDevice();
+ if (device.editor.isMobile) {
+ return (
+ <Footer>
+ <CustomFooter
+ excalidrawAPI={excalidrawAPI}
+ excalidrawLib={excalidrawLib}
+ />
+ </Footer>
+ );
+ }
+ return null;
+};
+export default MobileFooter;
diff --git a/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss
new file mode 100644
index 0000000..773a8ff
--- /dev/null
+++ b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.scss
@@ -0,0 +1,66 @@
+.sidebar {
+ height: 100%;
+ width: 0;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ left: 0;
+ background-color: #111;
+ overflow-x: hidden;
+ transition: 0.5s;
+ padding-top: 60px;
+
+ &.open {
+ width: 300px;
+ }
+
+ &-links {
+ display: flex;
+ flex-direction: column;
+ padding: 20px;
+
+ button {
+ padding: 10px;
+ margin: 10px;
+ background: #faa2c1;
+ color: #fff;
+ border: none;
+ cursor: pointer;
+ }
+ }
+}
+
+.sidebar a {
+ padding: 8px 8px 8px 32px;
+ text-decoration: none;
+ font-size: 25px;
+ color: #818181;
+ display: block;
+ transition: 0.3s;
+}
+
+.sidebar a:hover {
+ color: #f1f1f1;
+}
+
+.sidebar .closebtn {
+ position: absolute;
+ top: 0;
+ right: 0;
+ font-size: 36px;
+ margin-left: 50px;
+}
+
+.openbtn {
+ font-size: 20px;
+ cursor: pointer;
+ background-color: #111;
+ color: white;
+ padding: 10px 15px;
+ border: none;
+ display: flex;
+ margin-left: 50px;
+}
+.sidebar-open {
+ margin-left: 300px;
+}
diff --git a/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx
new file mode 100644
index 0000000..1939134
--- /dev/null
+++ b/examples/with-script-in-browser/components/sidebar/ExampleSidebar.tsx
@@ -0,0 +1,31 @@
+import React, { useState } from "react";
+import "./ExampleSidebar.scss";
+
+export default function Sidebar({ children }: { children: React.ReactNode }) {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+ <div id="mySidebar" className={`sidebar ${open ? "open" : ""}`}>
+ <button className="closebtn" onClick={() => setOpen(false)}>
+ x
+ </button>
+ <div className="sidebar-links">
+ <button>Empty Home</button>
+ <button>Empty About</button>
+ </div>
+ </div>
+ <div className={`${open ? "sidebar-open" : ""}`}>
+ <button
+ className="openbtn"
+ onClick={() => {
+ setOpen(!open);
+ }}
+ >
+ Open Sidebar
+ </button>
+ {children}
+ </div>
+ </>
+ );
+}
diff --git a/examples/with-script-in-browser/index.html b/examples/with-script-in-browser/index.html
new file mode 100644
index 0000000..dbbc4ca
--- /dev/null
+++ b/examples/with-script-in-browser/index.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ name="viewport"
+ content="width=device-width, initial-scale=1, shrink-to-fit=no"
+ />
+ <meta name="theme-color" content="#000000" />
+
+ <title>React App</title>
+ <script>
+ window.name = "codesandbox";
+ window.EXCALIDRAW_ASSET_PATH =
+ "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
+ </script>
+ </head>
+
+ <body>
+ <noscript> You need to enable JavaScript to run this app. </noscript>
+ <div id="root"></div>
+
+ <script type="module">
+ import * as ExcalidrawLib from "@excalidraw/excalidraw";
+
+ console.log(ExcalidrawLib);
+ window.ExcalidrawLib = ExcalidrawLib;
+ </script>
+ <script type="module" src="index.tsx"></script>
+ </body>
+</html>
diff --git a/examples/with-script-in-browser/index.tsx b/examples/with-script-in-browser/index.tsx
new file mode 100644
index 0000000..25ad96e
--- /dev/null
+++ b/examples/with-script-in-browser/index.tsx
@@ -0,0 +1,28 @@
+import App from "./components/ExampleApp";
+import React, { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+
+import type * as TExcalidraw from "@excalidraw/excalidraw";
+
+import "@excalidraw/excalidraw/index.css";
+
+declare global {
+ interface Window {
+ ExcalidrawLib: typeof TExcalidraw;
+ }
+}
+
+const rootElement = document.getElementById("root")!;
+const root = createRoot(rootElement);
+const { Excalidraw } = window.ExcalidrawLib;
+root.render(
+ <StrictMode>
+ <App
+ appTitle={"Excalidraw Example"}
+ useCustom={(api: any, args?: any[]) => {}}
+ excalidrawLib={window.ExcalidrawLib}
+ >
+ <Excalidraw />
+ </App>
+ </StrictMode>,
+);
diff --git a/examples/with-script-in-browser/initialData.tsx b/examples/with-script-in-browser/initialData.tsx
new file mode 100644
index 0000000..0db23d5
--- /dev/null
+++ b/examples/with-script-in-browser/initialData.tsx
@@ -0,0 +1,994 @@
+import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform";
+import type { FileId } from "@excalidraw/excalidraw/element/types";
+
+const elements: ExcalidrawElementSkeleton[] = [
+ {
+ type: "rectangle",
+ x: 10,
+ y: 10,
+ strokeWidth: 2,
+ id: "1",
+ },
+ {
+ type: "diamond",
+ x: 120,
+ y: 20,
+ backgroundColor: "#fff3bf",
+ strokeWidth: 2,
+ label: {
+ text: "HELLO EXCALIDRAW",
+ strokeColor: "#099268",
+ fontSize: 30,
+ },
+ id: "2",
+ },
+ {
+ type: "arrow",
+ x: 100,
+ y: 200,
+ label: { text: "HELLO WORLD!!" },
+ start: { type: "rectangle" },
+ end: { type: "ellipse" },
+ },
+ {
+ type: "image",
+ x: 606.1042326312408,
+ y: 153.57729779411773,
+ width: 230,
+ height: 230,
+ fileId: "rocket" as FileId,
+ },
+ {
+ type: "frame",
+ children: ["1", "2"],
+ name: "My frame",
+ },
+];
+export default {
+ elements,
+ appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 5 },
+ scrollToContent: true,
+ libraryItems: [
+ [
+ {
+ type: "line",
+
+ x: 209.72304760646858,
+ y: 338.83587294718825,
+ strokeColor: "#881fa3",
+ backgroundColor: "#be4bdb",
+ width: 116.42036295658873,
+ height: 103.65107323746608,
+ strokeSharpness: "sharp",
+ points: [
+ [-92.28090097254909, 7.105427357601002e-15],
+ [-154.72281841151394, 19.199290805487394],
+ [-155.45758928571422, 79.43840749607878],
+ [-99.89923520113778, 103.6510732374661],
+ [-40.317783799181804, 79.1587107641305],
+ [-39.037226329125524, 21.285677238400705],
+ [-92.28090097254909, 7.105427357601002e-15],
+ ],
+ },
+ ],
+ [
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ x: -249.48446738689245,
+ y: 374.851387389359,
+ strokeColor: "#0a11d3",
+ backgroundColor: "#228be6",
+ width: 88.21658171083376,
+ height: 113.8575037534261,
+ groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
+ points: [
+ [-0.22814350714115691, -43.414939319563715],
+ [0.06274947619197979, 42.63794490105306],
+ [-0.21453039840335475, 52.43469208825097],
+ [4.315205554872581, 56.66774540453215],
+ [20.089784992984285, 60.25027917349701],
+ [46.7532926683984, 61.365826671969444],
+ [72.22851104292477, 59.584691681394986],
+ [85.76368213524371, 55.325139565662596],
+ [87.67263486434864, 51.7342924478499],
+ [87.94074036468018, 43.84700272879395],
+ [87.73030872197806, -36.195582644606276],
+ [87.2559282533682, -43.758132174307036],
+ [81.5915337527493, -47.984890854524416],
+ [69.66352776578219, -50.4328058257654],
+ [42.481213744224995, -52.49167708145666],
+ [20.68789182864576, -51.26396751574663],
+ [3.5475921483286084, -47.099726468136254],
+ [-0.2758413461535838, -43.46664538034193],
+ [-0.22814350714115691, -43.414939319563715],
+ ],
+ },
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -249.02524930453623,
+ y: 398.8804363713438,
+ strokeColor: "#0a11d3",
+ backgroundColor: "transparent",
+ width: 88.30808627974527,
+ height: 9.797916664247975,
+ seed: 683951089,
+ groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, -2.1538602707609424],
+ [2.326538897826852, 1.751753055375216],
+ [12.359939318521995, 5.028526743934819],
+ [25.710950037209347, 7.012921076245119],
+ [46.6269757640547, 7.193749997581346],
+ [71.03526003420632, 5.930375670950649],
+ [85.2899738827162, 1.3342483900732343],
+ [88.30808627974527, -2.6041666666666288],
+ ],
+ },
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -250.11899081659772,
+ y: 365.80628180927204,
+ strokeColor: "#0a11d3",
+ backgroundColor: "transparent",
+ width: 88.30808627974527,
+ height: 9.797916664247975,
+ seed: 1817746897,
+ groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, -2.1538602707609424],
+ [2.326538897826852, 1.751753055375216],
+ [12.359939318521995, 5.028526743934819],
+ [25.710950037209347, 7.012921076245119],
+ [46.6269757640547, 7.193749997581346],
+ [71.03526003420632, 5.930375670950649],
+ [85.2899738827162, 1.3342483900732343],
+ [88.30808627974527, -2.6041666666666288],
+ ],
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -251.23981350275943,
+ y: 323.4117518426986,
+ strokeColor: "#0a11d3",
+ backgroundColor: "#fff",
+ width: 87.65074610854188,
+ height: 17.72670397681366,
+ seed: 1409727409,
+ groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
+ strokeSharpness: "sharp",
+ boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -179.73008120217884,
+ y: 347.98755471983213,
+ strokeColor: "#0a11d3",
+ backgroundColor: "#fff",
+ width: 12.846057046979809,
+ height: 13.941904362416096,
+ seed: 1073094033,
+ groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -179.73008120217884,
+ y: 378.5900085788926,
+ strokeColor: "#0a11d3",
+ backgroundColor: "#fff",
+ width: 12.846057046979809,
+ height: 13.941904362416096,
+ seed: 526271345,
+ groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -179.73008120217884,
+ y: 411.8508097533892,
+ strokeColor: "#0a11d3",
+ backgroundColor: "#fff",
+ width: 12.846057046979809,
+ height: 13.941904362416096,
+ seed: 243707217,
+ groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
+ strokeSharpness: "sharp",
+ },
+ ],
+ [
+ {
+ type: "diamond",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -109.55894395256101,
+ y: 381.22641397493356,
+ strokeColor: "#c92a2a",
+ backgroundColor: "#fd8888",
+ width: 112.64736525303451,
+ height: 36.77344700318558,
+ seed: 511870335,
+ groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "diamond",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -109.55894395256101,
+ y: 372.354634046675,
+ strokeColor: "#c92a2a",
+ backgroundColor: "#fd8888",
+ width: 112.64736525303451,
+ height: 36.77344700318558,
+ seed: 1283079231,
+ groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "diamond",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -109.55894395256101,
+ y: 359.72407445196296,
+ strokeColor: "#c92a2a",
+ backgroundColor: "#fd8888",
+ width: 112.64736525303451,
+ height: 36.77344700318558,
+ seed: 996251633,
+ groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "diamond",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -109.55894395256101,
+ y: 347.1924021546656,
+ strokeColor: "#c92a2a",
+ backgroundColor: "#fd8888",
+ width: 112.64736525303451,
+ height: 36.77344700318558,
+ seed: 1764842481,
+ groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
+ strokeSharpness: "sharp",
+ },
+ ],
+ [
+ {
+ type: "line",
+ fillStyle: "hachure",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 1.5707963267948957,
+ x: -471.6208001976387,
+ y: 520.7681448415112,
+ strokeColor: "#087f5b",
+ backgroundColor: "#40c057",
+ width: 52.317507746132115,
+ height: 154.56722543646003,
+ seed: 1424381745,
+ groupIds: ["HSrtfEf-CssQTf160Fb6R"],
+ strokeSharpness: "round",
+
+ points: [
+ [-0.24755378372925183, -40.169554027464216],
+ [-0.07503751055611152, 76.6515171914404],
+ [-0.23948042713317108, 89.95108885873196],
+ [2.446913573036335, 95.69766931810295],
+ [11.802146636255692, 100.56113713047068],
+ [27.615140546177496, 102.07554835500338],
+ [42.72341054254274, 99.65756899883291],
+ [50.75054563137204, 93.87501510096598],
+ [51.88266441510958, 89.00026150397161],
+ [52.04166639997853, 78.29287333983132],
+ [51.916868330459295, -30.36891819848148],
+ [51.635533423123285, -40.63545540065934],
+ [48.27622163143906, -46.37349057843314],
+ [41.202227904674494, -49.69665692879073],
+ [25.081551986374073, -52.49167708145666],
+ [12.15685839679867, -50.825000270901],
+ [1.9916746648394732, -45.171835889467935],
+ [-0.2758413461535838, -40.23974757720194],
+ [-0.24755378372925183, -40.169554027464216],
+ ],
+ },
+ {
+ type: "line",
+ version: 2405,
+ versionNonce: 2120341087,
+ isDeleted: false,
+ id: "TYsYe2VvJ60T_yKa3kyOw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 1.5707963267948957,
+ x: -496.3957643857249,
+ y: 541.7241190920508,
+ strokeColor: "#087f5b",
+ backgroundColor: "transparent",
+ width: 50.7174766392476,
+ height: 12.698053371678215,
+ seed: 726657713,
+ groupIds: ["HSrtfEf-CssQTf160Fb6R"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, -2.0205717204386002],
+ [1.3361877396713384, 3.0410845646550486],
+ [7.098613049589299, 7.287767671898479],
+ [14.766422451441104, 9.859533283467512],
+ [26.779003528407447, 10.093886705011586],
+ [40.79727342221974, 8.456559589697127],
+ [48.98410145879092, 2.500000505196364],
+ [50.7174766392476, -2.6041666666666288],
+ ],
+ },
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 1.5707963267948957,
+ x: -450.969983237283,
+ y: 542.1789894334747,
+ strokeColor: "#087f5b",
+ backgroundColor: "transparent",
+ width: 50.57247907260371,
+ height: 10.178760037658167,
+ seed: 1977326481,
+ groupIds: ["HSrtfEf-CssQTf160Fb6R"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, -2.136356936862347],
+ [1.332367676378171, 1.9210669226078037],
+ [7.078318632616268, 5.325208253515953],
+ [14.724206326638113, 7.386735659885842],
+ [26.70244431044034, 7.574593370991538],
+ [40.68063699304561, 6.262111896696538],
+ [48.84405948536458, 1.4873339211608216],
+ [50.57247907260371, -2.6041666666666288],
+ ],
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 1.5707963267948957,
+ x: -404.36521010516793,
+ y: 534.1894365757241,
+ strokeColor: "#087f5b",
+ backgroundColor: "#fff",
+ width: 51.27812853552538,
+ height: 22.797152568995934,
+ seed: 1774660383,
+ groupIds: ["HSrtfEf-CssQTf160Fb6R"],
+ strokeSharpness: "sharp",
+ boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
+ },
+ ],
+ [
+ {
+ type: "rectangle",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -393.3000561423187,
+ y: 338.9742643666818,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 70.67858069123133,
+ height: 107.25081879410921,
+ seed: 371096063,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "sharp",
+ boundElementIds: [
+ "CFu0B4Mw_1wC1Hbgx8Fs0",
+ "XIl_NhaFtRO00pX5Pq6VU",
+ "EndiSTFlx1AT7vcBVjgve",
+ ],
+ },
+ {
+ type: "rectangle",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -400.8474891780329,
+ y: 331.95417508096745,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 70.67858069123133,
+ height: 107.25081879410921,
+ seed: 685932433,
+ groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "sharp",
+ boundElementIds: [
+ "CFu0B4Mw_1wC1Hbgx8Fs0",
+ "XIl_NhaFtRO00pX5Pq6VU",
+ "EndiSTFlx1AT7vcBVjgve",
+ ],
+ },
+ {
+ type: "rectangle",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -410.24257846374826,
+ y: 323.7002688309677,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 70.67858069123133,
+ height: 107.25081879410921,
+ seed: 58634943,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "sharp",
+ boundElementIds: [
+ "CFu0B4Mw_1wC1Hbgx8Fs0",
+ "XIl_NhaFtRO00pX5Pq6VU",
+ "EndiSTFlx1AT7vcBVjgve",
+ ],
+ },
+ {
+ type: "draw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -398.2561518768373,
+ y: 371.84603609547054,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 46.57983585730082,
+ height: 3.249953844290203,
+ seed: 1673003743,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0.6014697828497827],
+ [40.42449133807562, 0.7588628355182573],
+ [46.57983585730082, -2.491091008771946],
+ ],
+ },
+ {
+ type: "draw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -396.400899638823,
+ y: 340.9822185794818,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 45.567415680676426,
+ height: 2.8032978840147194,
+ seed: 1821527807,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [16.832548902953302, -2.8032978840147194],
+ [45.567415680676426, -0.3275477042019195],
+ ],
+ },
+ {
+ type: "draw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -396.4774991551924,
+ y: 408.37659284983897,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 48.33668263438425,
+ height: 4.280657518731036,
+ seed: 1485707039,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [26.41225578429045, -0.2552319773002338],
+ [37.62000339651456, 2.3153712935189787],
+ [48.33668263438425, -1.9652862252120569],
+ ],
+ },
+ {
+ type: "draw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -399.6615463367227,
+ y: 419.61974125811776,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 54.40694982784246,
+ height: 2.9096445412231735,
+ seed: 1042012991,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [10.166093050596771, -1.166642430373031],
+ [16.130660965377448, -0.8422655250909383],
+ [46.26079588567538, 0.6125567455206506],
+ [54.40694982784246, -2.297087795702523],
+ ],
+ },
+ {
+ type: "draw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -399.3767034411569,
+ y: 356.042820132743,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 46.92865289294453,
+ height: 2.4757501798128,
+ seed: 295443295,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [18.193786115221407, -0.5912874140789839],
+ [46.92865289294453, 1.884462765733816],
+ ],
+ },
+ {
+ type: "draw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -399.26921524500654,
+ y: 390.5261491685826,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 46.92865289294453,
+ height: 2.4757501798128,
+ seed: 1734301567,
+ groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [8.093938105125233, 1.4279702913643746],
+ [18.193786115221407, -0.5912874140789839],
+ [46.92865289294453, 1.884462765733816],
+ ],
+ },
+ ],
+ [
+ {
+ type: "rectangle",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -593.9896997899341,
+ y: 343.9798351106279,
+ strokeColor: "#000000",
+ backgroundColor: "transparent",
+ width: 127.88383573213892,
+ height: 76.53703389977764,
+ seed: 106569279,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -595.0652975408293,
+ y: 354.6963695028721,
+ strokeColor: "#000000",
+ backgroundColor: "transparent",
+ width: 128.84193229844433,
+ height: 0,
+ seed: 73916127,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [128.84193229844433, 0],
+ ],
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 0,
+ opacity: 100,
+ angle: 0,
+ x: -589.5016643209792,
+ y: 348.2514049106367,
+ strokeColor: "#000000",
+ backgroundColor: "#fa5252",
+ width: 5.001953125,
+ height: 5.001953125,
+ seed: 387857791,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 0,
+ opacity: 100,
+ angle: 0,
+ x: -579.2389690084792,
+ y: 348.2514049106367,
+ strokeColor: "#000000",
+ backgroundColor: "#fab005",
+ width: 5.001953125,
+ height: 5.001953125,
+ seed: 1486370207,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 0,
+ opacity: 100,
+ angle: 0,
+ x: -568.525552542133,
+ y: 348.7021260644829,
+ strokeColor: "#000000",
+ backgroundColor: "#40c057",
+ width: 5.001953125,
+ height: 5.001953125,
+ seed: 610150847,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 90,
+ angle: 0,
+ x: -552.4984915525058,
+ y: 364.75449494249875,
+ strokeColor: "#000000",
+ backgroundColor: "#04aaf7",
+ width: 42.72020253937572,
+ height: 42.72020253937572,
+ seed: 144280593,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "draw",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ x: -530.327851842306,
+ y: 378.9357912947449,
+ strokeColor: "#087f5b",
+ backgroundColor: "#40c057",
+ width: 28.226201983883442,
+ height: 24.44112284281997,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "round",
+ points: [
+ [4.907524351775825, 2.043055712211473],
+ [3.0769604829149455, 1.6284171290602836],
+ [-2.66472604008681, -4.228569719133945],
+ [-6.450168189798415, -2.304577297733668],
+ [-7.704241049212052, 4.416384506147983],
+ [-6.361372181234263, 8.783101300254884],
+ [-12.516984713388897, 10.9291595737194],
+ [-12.295677738198286, 15.686226498407976],
+ [-7.473371426945252, 15.393030178104425],
+ [-3.787654025313423, 11.5207568827343],
+ [1.2873793872375165, 19.910682356036197],
+ [4.492232250183542, 20.212553123686025],
+ [1.1302787567009416, 6.843494873631317],
+ [6.294108177816019, 6.390688722156585],
+ [8.070028349098962, 7.910451897221202],
+ [14.143675334886687, 7.910451897221202],
+ [15.709217270494545, 2.6780252579576427],
+ [9.128749989671498, 3.1533849725326517],
+ [10.393751588600717, -3.7167773257046695],
+ [7.380151667177483, -3.30213874255348],
+ [4.669824267311791, 1.1200945145694894],
+ [4.907524351775825, 2.043055712211473],
+ ],
+ },
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 90,
+ angle: 0,
+ x: -551.4394290784783,
+ y: 385.71736850567976,
+ strokeColor: "#000000",
+ backgroundColor: "#99bcff",
+ width: 42.095115772272244,
+ height: 0,
+ seed: 1443027377,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [42.095115772272244, 0],
+ ],
+ },
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 0,
+ opacity: 90,
+ angle: 0,
+ x: -546.3441000487039,
+ y: 372.6245229061568,
+ strokeColor: "#000000",
+ backgroundColor: "#99bcff",
+ width: 29.31860660384862,
+ height: 5.711199931375845,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "round",
+ points: [
+ [0, -2.341683327443203],
+ [0.7724193963150375, -0.06510358900749044],
+ [4.103544916365185, 1.84492589414448],
+ [8.536129150893453, 3.0016281808630056],
+ [15.480325949120388, 3.1070332647092163],
+ [23.583965316012858, 2.3706131055211244],
+ [28.316582284417855, -0.3084668090492442],
+ [29.31860660384862, -2.6041666666666288],
+ ],
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 90,
+ angle: 0,
+ x: -538.2701841247845,
+ y: 363.37196531290607,
+ strokeColor: "#000000",
+ backgroundColor: "transparent",
+ width: 15.528434353116108,
+ height: 44.82230388130942,
+ seed: 683572113,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "line",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ opacity: 90,
+ x: -544.828148539078,
+ y: 402.0199316371545,
+ strokeColor: "#000000",
+ backgroundColor: "#99bcff",
+ width: 29.31860660384862,
+ height: 5.896061363392446,
+ seed: 318798801,
+ groupIds: ["TC0RSM64Cxmu17MlE12-o"],
+ strokeSharpness: "round",
+
+ points: [
+ [0, 0],
+ [4.103544916365185, -4.322122351104391],
+ [8.536129150893453, -5.516265043290966],
+ [15.480325949120388, -5.625081903117008],
+ [23.583965316012858, -4.8648251269605955],
+ [28.316582284417855, -2.0990281379671547],
+ [29.31860660384862, 0.2709794602754383],
+ ],
+ },
+ ],
+ [
+ {
+ type: "rectangle",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -715.1043446306466,
+ y: 330.4231266309418,
+ strokeColor: "#000000",
+ backgroundColor: "#ced4da",
+ width: 70.81644178885557,
+ height: 108.30428902193904,
+ seed: 1914896753,
+ groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "rectangle",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -706.996640540555,
+ y: 338.68030798133873,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 55.801163535143246,
+ height: 82.83278895375764,
+ seed: 1306468145,
+ groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "ellipse",
+ fillStyle: "solid",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -684.8099707762028,
+ y: 425.0579911039235,
+ strokeColor: "#000000",
+ backgroundColor: "#fff",
+ width: 11.427824006438863,
+ height: 11.427824006438863,
+ seed: 93422161,
+ groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "rectangle",
+ fillStyle: "cross-hatch",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: -698.7169501405845,
+ y: 349.2244646574789,
+ strokeColor: "#000000",
+ backgroundColor: "#fab005",
+ width: 39.2417827352022,
+ height: 19.889460471185775,
+ seed: 11646495,
+ groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
+ strokeSharpness: "sharp",
+ },
+ {
+ type: "rectangle",
+ fillStyle: "cross-hatch",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ x: -698.7169501405845,
+ y: 384.7822247024333,
+ strokeColor: "#000000",
+ backgroundColor: "#fab005",
+ width: 39.2417827352022,
+ height: 19.889460471185775,
+ seed: 291717649,
+ groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
+ strokeSharpness: "sharp",
+ },
+ ],
+ ],
+};
diff --git a/examples/with-script-in-browser/package.json b/examples/with-script-in-browser/package.json
new file mode 100644
index 0000000..3d61f1a
--- /dev/null
+++ b/examples/with-script-in-browser/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "with-script-in-browser",
+ "version": "1.0.0",
+ "private": true,
+ "dependencies": {
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "@excalidraw/excalidraw": "*",
+ "browser-fs-access": "0.29.1"
+ },
+ "devDependencies": {
+ "vite": "5.0.12",
+ "typescript": "^5"
+ },
+ "scripts": {
+ "start": "vite",
+ "build": "vite build",
+ "build:preview": "yarn build && vite preview --port 5002",
+ "build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
+ }
+}
diff --git a/examples/with-script-in-browser/public/images/doremon.png b/examples/with-script-in-browser/public/images/doremon.png
new file mode 100644
index 0000000..36208a4
--- /dev/null
+++ b/examples/with-script-in-browser/public/images/doremon.png
Binary files differ
diff --git a/examples/with-script-in-browser/public/images/excalibot.png b/examples/with-script-in-browser/public/images/excalibot.png
new file mode 100644
index 0000000..7928ec3
--- /dev/null
+++ b/examples/with-script-in-browser/public/images/excalibot.png
Binary files differ
diff --git a/examples/with-script-in-browser/public/images/pika.jpeg b/examples/with-script-in-browser/public/images/pika.jpeg
new file mode 100644
index 0000000..455ed52
--- /dev/null
+++ b/examples/with-script-in-browser/public/images/pika.jpeg
Binary files differ
diff --git a/examples/with-script-in-browser/public/images/rocket.jpeg b/examples/with-script-in-browser/public/images/rocket.jpeg
new file mode 100644
index 0000000..f17a74b
--- /dev/null
+++ b/examples/with-script-in-browser/public/images/rocket.jpeg
Binary files differ
diff --git a/examples/with-script-in-browser/tsconfig.json b/examples/with-script-in-browser/tsconfig.json
new file mode 100644
index 0000000..be262d1
--- /dev/null
+++ b/examples/with-script-in-browser/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "module": "ES2022",
+ "moduleResolution": "Bundler",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "skipLibCheck": true
+ }
+}
diff --git a/examples/with-script-in-browser/utils.ts b/examples/with-script-in-browser/utils.ts
new file mode 100644
index 0000000..a77b93f
--- /dev/null
+++ b/examples/with-script-in-browser/utils.ts
@@ -0,0 +1,145 @@
+import { unstable_batchedUpdates } from "react-dom";
+import { fileOpen as _fileOpen } from "browser-fs-access";
+import { MIME_TYPES } from "@excalidraw/excalidraw";
+
+type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
+
+const INPUT_CHANGE_INTERVAL_MS = 500;
+
+export type ResolvablePromise<T> = Promise<T> & {
+ resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
+ reject: (error: Error) => void;
+};
+export const resolvablePromise = <T>() => {
+ let resolve!: any;
+ let reject!: any;
+ const promise = new Promise((_resolve, _reject) => {
+ resolve = _resolve;
+ reject = _reject;
+ });
+ (promise as any).resolve = resolve;
+ (promise as any).reject = reject;
+ return promise as ResolvablePromise<T>;
+};
+
+export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
+ const xd = x2 - x1;
+ const yd = y2 - y1;
+ return Math.hypot(xd, yd);
+};
+
+export const fileOpen = <M extends boolean | undefined = false>(opts: {
+ extensions?: FILE_EXTENSION[];
+ description: string;
+ multiple?: M;
+}): Promise<M extends false | undefined ? File : File[]> => {
+ // an unsafe TS hack, alas not much we can do AFAIK
+ type RetType = M extends false | undefined ? File : File[];
+
+ const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
+ mimeTypes.push(MIME_TYPES[type]);
+
+ return mimeTypes;
+ }, [] as string[]);
+
+ const extensions = opts.extensions?.reduce((acc, ext) => {
+ if (ext === "jpg") {
+ return acc.concat(".jpg", ".jpeg");
+ }
+ return acc.concat(`.${ext}`);
+ }, [] as string[]);
+
+ return _fileOpen({
+ description: opts.description,
+ extensions,
+ mimeTypes,
+ multiple: opts.multiple ?? false,
+ legacySetup: (resolve, reject, input) => {
+ const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
+ const focusHandler = () => {
+ checkForFile();
+ document.addEventListener("keyup", scheduleRejection);
+ document.addEventListener("pointerup", scheduleRejection);
+ scheduleRejection();
+ };
+ const checkForFile = () => {
+ // this hack might not work when expecting multiple files
+ if (input.files?.length) {
+ const ret = opts.multiple ? [...input.files] : input.files[0];
+ resolve(ret as RetType);
+ }
+ };
+ requestAnimationFrame(() => {
+ window.addEventListener("focus", focusHandler);
+ });
+ const interval = window.setInterval(() => {
+ checkForFile();
+ }, INPUT_CHANGE_INTERVAL_MS);
+ return (rejectPromise) => {
+ clearInterval(interval);
+ scheduleRejection.cancel();
+ window.removeEventListener("focus", focusHandler);
+ document.removeEventListener("keyup", scheduleRejection);
+ document.removeEventListener("pointerup", scheduleRejection);
+ if (rejectPromise) {
+ // so that something is shown in console if we need to debug this
+ console.warn("Opening the file was canceled (legacy-fs).");
+ rejectPromise(new Error("Request Aborted"));
+ }
+ };
+ },
+ }) as Promise<RetType>;
+};
+
+export const debounce = <T extends any[]>(
+ fn: (...args: T) => void,
+ timeout: number,
+) => {
+ let handle = 0;
+ let lastArgs: T | null = null;
+ const ret = (...args: T) => {
+ lastArgs = args;
+ clearTimeout(handle);
+ handle = window.setTimeout(() => {
+ lastArgs = null;
+ fn(...args);
+ }, timeout);
+ };
+ ret.flush = () => {
+ clearTimeout(handle);
+ if (lastArgs) {
+ const _lastArgs = lastArgs;
+ lastArgs = null;
+ fn(..._lastArgs);
+ }
+ };
+ ret.cancel = () => {
+ lastArgs = null;
+ clearTimeout(handle);
+ };
+ return ret;
+};
+
+export const withBatchedUpdates = <
+ TFunction extends ((event: any) => void) | (() => void),
+>(
+ func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
+) =>
+ ((event) => {
+ unstable_batchedUpdates(func as TFunction, event);
+ }) as TFunction;
+
+/**
+ * barches React state updates and throttles the calls to a single call per
+ * animation frame
+ */
+export const withBatchedUpdatesThrottled = <
+ TFunction extends ((event: any) => void) | (() => void),
+>(
+ func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
+) => {
+ // @ts-ignore
+ return throttleRAF<Parameters<TFunction>>(((event) => {
+ unstable_batchedUpdates(func, event);
+ }) as TFunction);
+};
diff --git a/examples/with-script-in-browser/vercel.json b/examples/with-script-in-browser/vercel.json
new file mode 100644
index 0000000..99a5811
--- /dev/null
+++ b/examples/with-script-in-browser/vercel.json
@@ -0,0 +1,5 @@
+{
+ "outputDirectory": "dist",
+ "installCommand": "yarn install",
+ "buildCommand": "yarn build:package && yarn build"
+}
diff --git a/examples/with-script-in-browser/vite.config.mts b/examples/with-script-in-browser/vite.config.mts
new file mode 100644
index 0000000..3ff0ac6
--- /dev/null
+++ b/examples/with-script-in-browser/vite.config.mts
@@ -0,0 +1,19 @@
+import { defineConfig } from "vite";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ server: {
+ port: 3001,
+ // open the browser
+ open: true,
+ },
+ publicDir: "public",
+ optimizeDeps: {
+ esbuildOptions: {
+ // Bumping to 2022 due to "Arbitrary module namespace identifier names" not being
+ // supported in Vite's default browser target https://github.com/vitejs/vite/issues/13556
+ target: "es2022",
+ treeShaking: true,
+ },
+ },
+});