2
0
Эх сурвалжийг харах

Moved minimap rendering to offscreen canvas

tk338g 4 жил өмнө
parent
commit
6a8680f500

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 27940 - 1
package-lock.json


+ 2 - 1
package.json

@@ -60,7 +60,8 @@
     "lint-staged": "10.5.4",
     "pepjs": "0.5.3",
     "prettier": "2.2.1",
-    "rewire": "5.0.0"
+    "rewire": "5.0.0",
+    "worker-loader": "3.0.8"
   },
   "engines": {
     "node": ">=12.0.0"

+ 2 - 2
src/appState.ts

@@ -42,7 +42,7 @@ export const getDefaultAppState = (): Omit<
     exportEmbedScene: false,
     fileHandle: null,
     gridSize: null,
-    height: window.innerHeight,
+    height: globalThis.innerHeight,
     isBindingEnabled: true,
     isLibraryOpen: false,
     isLoading: false,
@@ -69,7 +69,7 @@ export const getDefaultAppState = (): Omit<
     suggestedBindings: [],
     toastMessage: null,
     viewBackgroundColor: oc.white,
-    width: window.innerWidth,
+    width: globalThis.innerWidth,
     zenModeEnabled: false,
     zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
     viewModeEnabled: false,

+ 4 - 0
src/components/ExportDialog.tsx

@@ -101,6 +101,10 @@ const ExportModal = ({
         shouldAddWatermark,
       });
 
+      if (canvas instanceof OffscreenCanvas) {
+        return;
+      }
+
       // if converting to blob fails, there's some problem that will
       // likely prevent preview and export (e.g. canvas too big)
       canvasToBlob(canvas)

+ 37 - 25
src/components/MiniMap.tsx

@@ -1,12 +1,13 @@
 import "./MiniMap.scss";
 
-import React, { useEffect, useRef, useMemo } from "react";
+import React, { useEffect, useRef, useMemo, useState } from "react";
 import { getCommonBounds, getNonDeletedElements } from "../element";
 import { ExcalidrawElement } from "../element/types";
-import { exportToCanvas } from "../scene/export";
 import { AppState } from "../types";
 import { distance, viewportCoordsToSceneCoords } from "../utils";
 import { Island } from "./Island";
+// eslint-disable-next-line import/no-webpack-loader-syntax
+import MinimapWorker from "worker-loader!../renderer/minimapWorker";
 
 const RATIO = 1.2;
 const MINIMAP_HEIGHT = 150;
@@ -89,36 +90,47 @@ export function MiniMap({
   appState: AppState;
   elements: readonly ExcalidrawElement[];
 }) {
+  const [minimapWorker] = useState(() => new MinimapWorker());
   const canvasRef = useRef<HTMLCanvasElement>(null);
-  const appStateRef = useRef<AppState>(appState);
+  const elementsRef = useRef(elements);
+  elementsRef.current = elements;
+  const appStateRef = useRef(appState);
   appStateRef.current = appState;
 
   useEffect(() => {
-    const canvasNode = canvasRef.current;
-    if (!canvasNode) {
+    const canvas = canvasRef.current;
+    if (!canvas) {
       return;
     }
 
-    exportToCanvas(
-      getNonDeletedElements(elements),
-      appStateRef.current,
-      {
-        exportBackground: true,
-        viewBackgroundColor: appStateRef.current.viewBackgroundColor,
-        shouldAddWatermark: false,
-      },
-      (width, height) => {
-        const scale = Math.min(MINIMAP_WIDTH / width, MINIMAP_HEIGHT / height);
-        canvasNode.width = width * scale;
-        canvasNode.height = height * scale;
-
-        return {
-          canvas: canvasNode,
-          scale,
-        };
-      },
-    );
-  }, [elements]);
+    const offscreenCanvas = canvas.transferControlToOffscreen();
+
+    minimapWorker.postMessage({ type: "INIT", canvas: offscreenCanvas }, [
+      offscreenCanvas,
+    ]);
+
+    minimapWorker.postMessage({
+      type: "DRAW",
+      elements: elementsRef.current,
+      appState: appStateRef.current,
+      width: MINIMAP_WIDTH,
+      height: MINIMAP_HEIGHT,
+    });
+
+    setInterval(() => {
+      minimapWorker.postMessage({
+        type: "DRAW",
+        elements: elementsRef.current,
+        appState: appStateRef.current,
+        width: MINIMAP_WIDTH,
+        height: MINIMAP_HEIGHT,
+      });
+    }, 1000);
+
+    return () => {
+      minimapWorker.terminate();
+    };
+  }, [minimapWorker]);
 
   return (
     <Island padding={1} className="MiniMap">

+ 5 - 0
src/data/index.ts

@@ -73,6 +73,11 @@ export const exportCanvas = async (
     scale,
     shouldAddWatermark,
   });
+
+  if (tempCanvas instanceof OffscreenCanvas) {
+    return;
+  }
+
   tempCanvas.style.display = "none";
   document.body.appendChild(tempCanvas);
 

+ 11 - 0
src/global.d.ts

@@ -88,3 +88,14 @@ interface Blob {
   handle?: import("browser-fs-acces").FileSystemHandle;
   name?: string;
 }
+
+declare module "worker-loader!*" {
+  // You need to change `Worker`, if you specified a different value for the `workerType` option
+  class WebpackWorker extends Worker {
+    constructor();
+  }
+
+  // Uncomment this if you set the `esModule` option to `false`
+  // export = WebpackWorker;
+  export default WebpackWorker;
+}

+ 4 - 2
src/keys.ts

@@ -1,5 +1,7 @@
-export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
-export const isWindows = /^Win/.test(window.navigator.platform);
+export const isDarwin = /Mac|iPod|iPhone|iPad/.test(
+  globalThis.navigator.platform,
+);
+export const isWindows = /^Win/.test(globalThis.navigator.platform);
 
 export const CODES = {
   EQUAL: "Equal",

+ 3 - 0
src/packages/utils.ts

@@ -48,6 +48,9 @@ export const exportToBlob = (
   },
 ): Promise<Blob | null> => {
   const canvas = exportToCanvas(opts);
+  if (canvas instanceof OffscreenCanvas) {
+    return Promise.resolve(null);
+  }
 
   let { mimeType = "image/png", quality } = opts;
 

+ 48 - 0
src/renderer/minimapWorker.ts

@@ -0,0 +1,48 @@
+/* eslint-disable no-restricted-globals */
+
+import { getNonDeletedElements } from "../element";
+import { ExcalidrawElement } from "../element/types";
+import { exportToCanvas } from "../scene/export";
+import { AppState } from "../types";
+
+const ctx: Worker = self as any;
+let canvas: OffscreenCanvas;
+
+ctx.addEventListener("message", (ev) => {
+  if (ev.data.type === "INIT") {
+    canvas = ev.data.canvas;
+  }
+
+  if (ev.data.type === "DRAW") {
+    const { elements, appState, width, height } = ev.data;
+    drawScene(canvas, elements, appState, width, height);
+  }
+});
+
+function drawScene(
+  canvas: OffscreenCanvas,
+  elements: readonly ExcalidrawElement[],
+  appState: AppState,
+  minimapWidth: number,
+  minimapHeight: number,
+) {
+  exportToCanvas(
+    getNonDeletedElements(elements),
+    appState,
+    {
+      exportBackground: true,
+      viewBackgroundColor: appState.viewBackgroundColor,
+      shouldAddWatermark: false,
+    },
+    (width, height) => {
+      const scale = Math.min(minimapWidth / width, minimapHeight / height);
+      canvas.width = width * scale;
+      canvas.height = height * scale;
+
+      return {
+        canvas,
+        scale,
+      };
+    },
+  );
+}

+ 16 - 10
src/renderer/renderElement.ts

@@ -115,7 +115,7 @@ const generateElementCanvas = (
 const drawElementOnCanvas = (
   element: NonDeletedExcalidrawElement,
   rc: RoughCanvas,
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
 ) => {
   context.globalAlpha = element.opacity / 100;
   switch (element.type) {
@@ -136,13 +136,16 @@ const drawElementOnCanvas = (
     default: {
       if (isTextElement(element)) {
         const rtl = isRTL(element.text);
-        const shouldTemporarilyAttach = rtl && !context.canvas.isConnected;
-        if (shouldTemporarilyAttach) {
-          // to correctly render RTL text mixed with LTR, we have to append it
-          // to the DOM
-          document.body.appendChild(context.canvas);
+        let shouldTemporarilyAttach = false;
+        if (!(context instanceof OffscreenCanvasRenderingContext2D)) {
+          shouldTemporarilyAttach = rtl && !context.canvas.isConnected;
+          if (shouldTemporarilyAttach) {
+            // to correctly render RTL text mixed with LTR, we have to append it
+            // to the DOM
+            document.body.appendChild(context.canvas);
+          }
+          context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
         }
-        context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
         const font = context.font;
         context.font = getFontString(element);
         const fillStyle = context.fillStyle;
@@ -170,7 +173,10 @@ const drawElementOnCanvas = (
         context.fillStyle = fillStyle;
         context.font = font;
         context.textAlign = textAlign;
-        if (shouldTemporarilyAttach) {
+        if (
+          shouldTemporarilyAttach &&
+          !(context instanceof OffscreenCanvasRenderingContext2D)
+        ) {
           context.canvas.remove();
         }
       } else {
@@ -451,7 +457,7 @@ const generateElementWithCanvas = (
 const drawElementFromCanvas = (
   elementWithCanvas: ExcalidrawElementWithCanvas,
   rc: RoughCanvas,
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   sceneState: SceneState,
 ) => {
   const element = elementWithCanvas.element;
@@ -482,7 +488,7 @@ const drawElementFromCanvas = (
 export const renderElement = (
   element: NonDeletedExcalidrawElement,
   rc: RoughCanvas,
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   renderOptimizations: boolean,
   sceneState: SceneState,
 ) => {

+ 12 - 12
src/renderer/renderScene.ts

@@ -53,7 +53,7 @@ import { UserIdleState } from "../excalidraw-app/collab/types";
 const hasEmojiSupport = supportsEmoji();
 
 const strokeRectWithRotation = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   x: number,
   y: number,
   width: number,
@@ -74,7 +74,7 @@ const strokeRectWithRotation = (
 };
 
 const strokeDiamondWithRotation = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   width: number,
   height: number,
   cx: number,
@@ -95,7 +95,7 @@ const strokeDiamondWithRotation = (
 };
 
 const strokeEllipseWithRotation = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   width: number,
   height: number,
   cx: number,
@@ -108,7 +108,7 @@ const strokeEllipseWithRotation = (
 };
 
 const fillCircle = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   cx: number,
   cy: number,
   radius: number,
@@ -120,7 +120,7 @@ const fillCircle = (
 };
 
 const strokeGrid = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   gridSize: number,
   offsetX: number,
   offsetY: number,
@@ -143,7 +143,7 @@ const strokeGrid = (
 };
 
 const renderLinearPointHandles = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   appState: AppState,
   sceneState: SceneState,
   element: NonDeleted<ExcalidrawLinearElement>,
@@ -182,7 +182,7 @@ export const renderScene = (
   selectionElement: NonDeletedExcalidrawElement | null,
   scale: number,
   rc: RoughCanvas,
-  canvas: HTMLCanvasElement,
+  canvas: HTMLCanvasElement | OffscreenCanvas,
   sceneState: SceneState,
   // extra options, currently passed by export helper
   {
@@ -573,7 +573,7 @@ export const renderScene = (
 };
 
 const renderTransformHandles = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   sceneState: SceneState,
   transformHandles: TransformHandles,
   angle: number,
@@ -609,7 +609,7 @@ const renderTransformHandles = (
 };
 
 const renderSelectionBorder = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   sceneState: SceneState,
   elementProperties: {
     angle: number;
@@ -671,7 +671,7 @@ const renderSelectionBorder = (
 };
 
 const renderBindingHighlight = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   sceneState: SceneState,
   suggestedBinding: SuggestedBinding,
 ) => {
@@ -693,7 +693,7 @@ const renderBindingHighlight = (
 };
 
 const renderBindingHighlightForBindableElement = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   element: ExcalidrawBindableElement,
 ) => {
   const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
@@ -748,7 +748,7 @@ const renderBindingHighlightForBindableElement = (
 };
 
 const renderBindingHighlightForSuggestedPointBinding = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   suggestedBinding: SuggestedPointBinding,
 ) => {
   const [element, startOrEnd, bindableElement] = suggestedBinding;

+ 1 - 1
src/renderer/roundRect.ts

@@ -9,7 +9,7 @@
  * @param {Number} radius The corner radius
  */
 export const roundRect = (
-  context: CanvasRenderingContext2D,
+  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
   x: number,
   y: number,
   width: number,

+ 5 - 2
src/scene/export.ts

@@ -32,7 +32,10 @@ export const exportToCanvas = (
   createCanvas: (
     width: number,
     height: number,
-  ) => { canvas: HTMLCanvasElement; scale: number } = (width, height) => {
+  ) => { canvas: HTMLCanvasElement | OffscreenCanvas; scale: number } = (
+    width,
+    height,
+  ) => {
     const tempCanvas = document.createElement("canvas");
     tempCanvas.width = width * scale;
     tempCanvas.height = height * scale;
@@ -57,7 +60,7 @@ export const exportToCanvas = (
     appState,
     null,
     newScale,
-    rough.canvas(tempCanvas),
+    rough.canvas((tempCanvas as unknown) as HTMLCanvasElement),
     tempCanvas,
     {
       viewBackgroundColor: exportBackground ? viewBackgroundColor : null,

+ 3 - 0
src/utils.ts

@@ -372,6 +372,9 @@ export const getVersion = () => {
 
 // Adapted from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/emoji.js
 export const supportsEmoji = () => {
+  if (typeof document === "undefined") {
+    return;
+  }
   const canvas = document.createElement("canvas");
   const ctx = canvas.getContext("2d");
   if (!ctx) {

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно