Moved minimap rendering to offscreen canvas

This commit is contained in:
tk338g 2021-02-19 21:07:09 +03:00
parent 3b0aff0ac6
commit 6a8680f500
15 changed files with 28144 additions and 175 deletions

28113
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
import "./MiniMap.scss"; import "./MiniMap.scss";
import React, { useEffect, useRef, useMemo } from "react"; import React, { useEffect, useRef, useMemo, useState } from "react";
import { getCommonBounds, getNonDeletedElements } from "../element"; import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { exportToCanvas } from "../scene/export";
import { AppState } from "../types"; import { AppState } from "../types";
import { distance, viewportCoordsToSceneCoords } from "../utils"; import { distance, viewportCoordsToSceneCoords } from "../utils";
import { Island } from "./Island"; 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 RATIO = 1.2;
const MINIMAP_HEIGHT = 150; const MINIMAP_HEIGHT = 150;
@ -89,36 +90,47 @@ export function MiniMap({
appState: AppState; appState: AppState;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
}) { }) {
const [minimapWorker] = useState(() => new MinimapWorker());
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const appStateRef = useRef<AppState>(appState); const elementsRef = useRef(elements);
elementsRef.current = elements;
const appStateRef = useRef(appState);
appStateRef.current = appState; appStateRef.current = appState;
useEffect(() => { useEffect(() => {
const canvasNode = canvasRef.current; const canvas = canvasRef.current;
if (!canvasNode) { if (!canvas) {
return; return;
} }
exportToCanvas( const offscreenCanvas = canvas.transferControlToOffscreen();
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 { minimapWorker.postMessage({ type: "INIT", canvas: offscreenCanvas }, [
canvas: canvasNode, offscreenCanvas,
scale, ]);
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]);
);
}, [elements]);
return ( return (
<Island padding={1} className="MiniMap"> <Island padding={1} className="MiniMap">

View File

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

11
src/global.d.ts vendored
View File

@ -88,3 +88,14 @@ interface Blob {
handle?: import("browser-fs-acces").FileSystemHandle; handle?: import("browser-fs-acces").FileSystemHandle;
name?: string; 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;
}

View File

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

View File

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

View File

@ -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,
};
},
);
}

View File

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

View File

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

View File

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

View File

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

View File

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