Compare commits

...

8 Commits

Author SHA1 Message Date
232412d7bc Test fixup 2021-02-19 21:17:58 +03:00
6a8680f500 Moved minimap rendering to offscreen canvas 2021-02-19 21:07:09 +03:00
3b0aff0ac6 Fix minimap rerendering on scroll 2021-02-19 20:12:48 +03:00
93d0a56bdb Check for NaN before applying styles 2021-02-19 20:12:48 +03:00
0a3675e1b9 Update test snapshots 2021-02-19 20:12:48 +03:00
cf35caaf23 Toggle minimap with "M" key 2021-02-19 20:12:48 +03:00
4e3bf7e8d2 Use one canvas for minimap and preserve viewport borders 2021-02-19 20:12:47 +03:00
4c3544df4a Simple minimap implementation 2021-02-19 20:12:47 +03:00
23 changed files with 28365 additions and 152 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",
"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"
@ -75,6 +76,9 @@
"transformIgnorePatterns": [
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
],
"moduleNameMapper": {
"^worker-loader!.+": "<rootDir>/src/__mocks__/worker-mock.js"
},
"resetMocks": false
},
"name": "excalidraw",

View File

@ -0,0 +1,4 @@
module.exports = class {
postMessage() {}
terminate() {}
};

View File

@ -7,6 +7,7 @@ import { register } from "./register";
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
import { CODES, KEYS } from "../keys";
import { HelpIcon } from "../components/HelpIcon";
import { MiniMap } from "../components/MiniMap";
export const actionToggleCanvasMenu = register({
name: "toggleCanvasMenu",
@ -84,3 +85,21 @@ export const actionShortcuts = register({
),
keyTest: (event) => event.key === KEYS.QUESTION_MARK,
});
export const actionMinimap = register({
name: "toggleMinimap",
perform: (_elements, appState) => {
return {
appState: {
...appState,
isMinimapEnabled: !appState.isMinimapEnabled,
},
commitToHistory: false,
};
},
PanelComponent: ({ appState, elements }) =>
appState.isMinimapEnabled ? (
<MiniMap appState={appState} elements={elements} />
) : null,
keyTest: (event) => event.key === KEYS.M,
});

View File

@ -44,6 +44,7 @@ export {
actionToggleEditMenu,
actionFullScreen,
actionShortcuts,
actionMinimap,
} from "./actionMenu";
export { actionGroup, actionUngroup } from "./actionGroup";

View File

@ -85,7 +85,8 @@ export type ActionName =
| "alignHorizontallyCentered"
| "distributeHorizontally"
| "distributeVertically"
| "viewMode";
| "viewMode"
| "toggleMinimap";
export interface Action {
name: ActionName;

View File

@ -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,10 +69,11 @@ 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,
isMinimapEnabled: false,
};
};
@ -153,6 +154,7 @@ const APP_STATE_STORAGE_CONF = (<
zenModeEnabled: { browser: true, export: false },
zoom: { browser: true, export: false },
viewModeEnabled: { browser: false, export: false },
isMinimapEnabled: { browser: true, export: false },
});
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(

View File

@ -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)

View File

@ -602,6 +602,7 @@ const LayerUI = ({
>
{renderCustomFooter?.(false)}
{actionManager.renderAction("toggleShortcuts")}
{actionManager.renderAction("toggleMinimap")}
</div>
<button
className={clsx("disable-zen-mode", {

View File

@ -0,0 +1,5 @@
.Island.MiniMap {
position: absolute;
bottom: 50px;
right: calc(var(--space-factor) * 4);
}

151
src/components/MiniMap.tsx Normal file
View File

@ -0,0 +1,151 @@
import "./MiniMap.scss";
import React, { useEffect, useRef, useMemo, useState } from "react";
import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
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;
const MINIMAP_WIDTH = MINIMAP_HEIGHT * RATIO;
const MinimapViewport = ({
elements,
appState,
}: {
elements: readonly ExcalidrawElement[];
appState: AppState;
}) => {
const [minX, minY, maxX, maxY] = useMemo(
() => getCommonBounds(getNonDeletedElements(elements)),
[elements],
);
const minimapScale = Math.min(
MINIMAP_WIDTH / distance(minX, maxX),
MINIMAP_HEIGHT / distance(minY, maxY),
);
const leftTop = viewportCoordsToSceneCoords(
{ clientX: 0, clientY: 0 },
appState,
);
const rightBot = viewportCoordsToSceneCoords(
{ clientX: appState.width, clientY: appState.height },
appState,
);
const top = (leftTop.y - minY) * minimapScale;
const left = (leftTop.x - minX) * minimapScale;
const width = (rightBot.x - leftTop.x) * minimapScale;
const height = (rightBot.y - leftTop.y) * minimapScale;
// Set viewport boundaries
const viewportTop = Math.min(Math.max(0, top), MINIMAP_HEIGHT);
const viewportLeft = Math.min(Math.max(0, left), MINIMAP_WIDTH);
const viewportWidth = Math.min(
MINIMAP_WIDTH - viewportLeft,
width,
width + left,
);
const viewportHeight = Math.min(
MINIMAP_HEIGHT - viewportTop,
height,
height + top,
);
if (
Number.isNaN(viewportTop) ||
Number.isNaN(viewportLeft) ||
Number.isNaN(viewportWidth) ||
Number.isNaN(viewportHeight)
) {
return null;
}
return (
<div
style={{
border: "2px solid orange",
boxSizing: "border-box",
position: "absolute",
pointerEvents: "none",
top: viewportTop,
left: viewportLeft,
width: viewportWidth,
height: viewportHeight,
}}
/>
);
};
export function MiniMap({
appState,
elements,
}: {
appState: AppState;
elements: readonly ExcalidrawElement[];
}) {
const [minimapWorker] = useState(() => new MinimapWorker());
const canvasRef = useRef<HTMLCanvasElement>(null);
const elementsRef = useRef(elements);
elementsRef.current = elements;
const appStateRef = useRef(appState);
appStateRef.current = appState;
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
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">
<div
style={{
width: MINIMAP_WIDTH,
height: MINIMAP_HEIGHT,
position: "relative",
overflow: "hidden",
backgroundColor: appState.viewBackgroundColor,
}}
>
<canvas ref={canvasRef} />
<MinimapViewport elements={elements} appState={appState} />
</div>
</Island>
);
}

View File

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

11
src/global.d.ts vendored
View File

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

View File

@ -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",
@ -43,6 +45,7 @@ export const KEYS = {
D: "d",
E: "e",
L: "l",
M: "m",
O: "o",
P: "p",
Q: "q",

View File

@ -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;

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 = (
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,
) => {

View File

@ -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;

View File

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

View File

@ -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,

View File

@ -35,6 +35,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -495,6 +496,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -961,6 +963,7 @@ Object {
"isBindingEnabled": false,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -1736,6 +1739,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -1939,6 +1943,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -2396,6 +2401,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -2648,6 +2654,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -2811,6 +2818,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -3287,6 +3295,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -3594,6 +3603,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -3797,6 +3807,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -4040,6 +4051,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -4291,6 +4303,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -4673,6 +4686,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -4967,6 +4981,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -5273,6 +5288,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -5480,6 +5496,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -5643,6 +5660,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -6095,6 +6113,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -6412,6 +6431,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -8445,6 +8465,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -8806,6 +8827,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -9060,6 +9082,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -9312,6 +9335,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -9626,6 +9650,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -9789,6 +9814,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -9952,6 +9978,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -10115,6 +10142,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -10308,6 +10336,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -10501,6 +10530,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -10694,6 +10724,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -10887,6 +10918,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -11050,6 +11082,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -11213,6 +11246,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -11406,6 +11440,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -11569,6 +11604,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -11762,6 +11798,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -12477,6 +12514,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -12729,6 +12767,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "touch",
@ -12830,6 +12869,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -12929,6 +12969,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -13092,6 +13133,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -13399,6 +13441,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -13706,6 +13749,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -13869,6 +13913,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -14064,6 +14109,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -14312,6 +14358,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -14635,6 +14682,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -15473,6 +15521,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -15780,6 +15829,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -16087,6 +16137,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -16465,6 +16516,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -16631,6 +16683,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -16951,6 +17004,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -17189,6 +17243,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -17443,6 +17498,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -17769,6 +17825,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -17868,6 +17925,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -18031,6 +18089,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -18851,6 +18910,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -18950,6 +19010,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -19703,6 +19764,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -20107,6 +20169,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -20379,6 +20442,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "touch",
@ -20480,6 +20544,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -20977,6 +21042,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
@ -21076,6 +21142,7 @@ Object {
"isBindingEnabled": true,
"isLibraryOpen": false,
"isLoading": false,
"isMinimapEnabled": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",

View File

@ -117,6 +117,7 @@ export type AppState = {
shown: true;
data: Spreadsheet;
};
isMinimapEnabled: boolean;
};
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };

View File

@ -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) {