Compare commits

..
15 changed files with 196 additions and 212 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
An open source virtual hand-drawn style whiteboard. </br>
Collaborative and end-to-end encrypted. </br>
<br />
</h3>
</h2>
</div>
<br />
+1
View File
@@ -20,6 +20,7 @@
},
"dependencies": {
"@dwelle/tunnel-rat": "0.1.1",
"@excalidraw/roughjs": "4.5.3",
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2",
+1 -1
View File
@@ -94,7 +94,7 @@
// if its present redirect
document.cookie.includes("excplus-autoredirect=true")
) {
window.location.href = "https://app.excalidraw.com";
// window.location.href = "https://app.excalidraw.com";
}
</script>
+90 -86
View File
@@ -23,6 +23,7 @@ import {
ExcalidrawTextElement,
} from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { getFontString } from "../utils";
import { register } from "./register";
@@ -185,107 +186,110 @@ export const actionCreateContainerFromText = register({
trackEvent: { category: "element" },
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1 && isTextElement(selectedElements[0]);
const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements;
},
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const updatedElements = elements.slice();
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
const textElement = selectedElements[0];
const container = newElement({
type: "rectangle",
backgroundColor: appState.currentItemBackgroundColor,
boundElements: [
...(textElement.boundElements || []),
{ id: textElement.id, type: "text" },
],
angle: textElement.angle,
fillStyle: appState.currentItemFillStyle,
strokeColor: appState.currentItemStrokeColor,
roughness: appState.currentItemRoughness,
strokeWidth: appState.currentItemStrokeWidth,
strokeStyle: appState.currentItemStrokeStyle,
roundness:
appState.currentItemRoundness === "round"
? {
type: isUsingAdaptiveRadius("rectangle")
? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS,
}
: null,
opacity: 100,
locked: false,
x: textElement.x - BOUND_TEXT_PADDING,
y: textElement.y - BOUND_TEXT_PADDING,
width: computeContainerDimensionForBoundText(
textElement.width,
"rectangle",
),
height: computeContainerDimensionForBoundText(
textElement.height,
"rectangle",
),
groupIds: textElement.groupIds,
});
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: AppState["selectedElementIds"] = {};
// update bindings
if (textElement.boundElements?.length) {
const linearElementIds = textElement.boundElements
.filter((ele) => ele.type === "arrow")
.map((el) => el.id);
const linearElements = updatedElements.filter((ele) =>
linearElementIds.includes(ele.id),
) as ExcalidrawLinearElement[];
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
if (startBinding?.elementId === textElement.id) {
startBinding = {
...startBinding,
elementId: container.id,
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding });
}
for (const textElement of selectedElements) {
if (isTextElement(textElement)) {
const container = newElement({
type: "rectangle",
backgroundColor: appState.currentItemBackgroundColor,
boundElements: [
...(textElement.boundElements || []),
{ id: textElement.id, type: "text" },
],
angle: textElement.angle,
fillStyle: appState.currentItemFillStyle,
strokeColor: appState.currentItemStrokeColor,
roughness: appState.currentItemRoughness,
strokeWidth: appState.currentItemStrokeWidth,
strokeStyle: appState.currentItemStrokeStyle,
roundness:
appState.currentItemRoundness === "round"
? {
type: isUsingAdaptiveRadius("rectangle")
? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS,
}
: null,
opacity: 100,
locked: false,
x: textElement.x - BOUND_TEXT_PADDING,
y: textElement.y - BOUND_TEXT_PADDING,
width: computeContainerDimensionForBoundText(
textElement.width,
"rectangle",
),
height: computeContainerDimensionForBoundText(
textElement.height,
"rectangle",
),
groupIds: textElement.groupIds,
});
}
mutateElement(textElement, {
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
});
redrawTextBoundingBox(textElement, container);
// update bindings
if (textElement.boundElements?.length) {
const linearElementIds = textElement.boundElements
.filter((ele) => ele.type === "arrow")
.map((el) => el.id);
const linearElements = updatedElements.filter((ele) =>
linearElementIds.includes(ele.id),
) as ExcalidrawLinearElement[];
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
return {
elements: pushContainerBelowText(
[...elements, container],
if (startBinding?.elementId === textElement.id) {
startBinding = {
...startBinding,
elementId: container.id,
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding }, false);
}
});
}
mutateElement(
textElement,
{
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
},
false,
);
redrawTextBoundingBox(textElement, container);
updatedElements = pushContainerBelowText(
[...updatedElements, container],
container,
textElement,
),
appState: {
...appState,
selectedElementIds: {
[container.id]: true,
[textElement.id]: false,
},
},
commitToHistory: true,
};
);
containerIds[container.id] = true;
}
}
return {
elements: updatedElements,
appState,
appState: {
...appState,
selectedElementIds: containerIds,
},
commitToHistory: true,
};
},
+4 -11
View File
@@ -1,8 +1,8 @@
import React, { useContext } from "react";
import { flushSync } from "react-dom";
import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import { RoughCanvas } from "@excalidraw/roughjs/bin/canvas";
import rough from "@excalidraw/roughjs/bin/rough";
import clsx from "clsx";
import { nanoid } from "nanoid";
@@ -1361,10 +1361,6 @@ class App extends React.Component<AppProps, AppState> {
document.querySelector(".excalidraw")!,
).getPropertyValue("--color-selection");
//const now = Date.now();
//if (!this.state.shouldCacheIgnoreZoom) {
// console.log(`renderScene`, now);
//}
renderScene(
{
elements: renderingElements,
@@ -1401,14 +1397,11 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
this.scheduleImageRefresh();
//if (!this.state.shouldCacheIgnoreZoom) {
// setTimeout(() => console.log(`after renderScene`, now));
//}
},
},
true ||
(THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true),
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
);
if (!THROTTLE_NEXT_RENDER) {
+2 -2
View File
@@ -7,8 +7,8 @@ import {
ExcalidrawTextElementWithContainer,
} from "./types";
import { distance2d, rotate } from "../math";
import rough from "roughjs/bin/rough";
import { Drawable, Op } from "roughjs/bin/core";
import rough from "@excalidraw/roughjs/bin/rough";
import { Drawable, Op } from "@excalidraw/roughjs/bin/core";
import { Point } from "../types";
import {
getShapeForElement,
+1 -1
View File
@@ -30,7 +30,7 @@ import {
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
import { Point } from "../types";
import { Drawable } from "roughjs/bin/core";
import { Drawable } from "@excalidraw/roughjs/bin/core";
import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement";
import { hasBoundTextElement, isImageElement } from "./typeChecks";
+1
View File
@@ -297,6 +297,7 @@ export const textWysiwyg = ({
filter: "var(--theme-filter)",
maxHeight: `${editorMaxHeight}px`,
});
editable.scrollTop = 0;
// For some reason updating font attribute doesn't set font family
// hence updating font family explicitly for test environment
if (isTestEnv()) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { Random } from "roughjs/bin/math";
import { Random } from "@excalidraw/roughjs/bin/math";
import { nanoid } from "nanoid";
import { isTestEnv } from "./utils";
+76 -61
View File
@@ -21,16 +21,16 @@ import {
getElementAbsoluteCoords,
getArrowheadPoints,
} from "../element/bounds";
import { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable, Options } from "roughjs/bin/core";
import { RoughSVG } from "roughjs/bin/svg";
import { RoughGenerator } from "roughjs/bin/generator";
import { RoughCanvas } from "@excalidraw/roughjs/bin/canvas";
import { Drawable, Options } from "@excalidraw/roughjs/bin/core";
import { RoughSVG } from "@excalidraw/roughjs/bin/svg";
import { RoughGenerator } from "@excalidraw/roughjs/bin/generator";
import { RenderConfig } from "../scene/types";
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
import rough from "roughjs/bin/rough";
import { AppState, BinaryFiles, NormalizedZoomValue, Zoom } from "../types";
import rough from "@excalidraw/roughjs/bin/rough";
import { AppState, BinaryFiles, Zoom } from "../types";
import { getDefaultAppState } from "../appState";
import {
BOUND_TEXT_PADDING,
@@ -87,54 +87,64 @@ export interface ExcalidrawElementWithCanvas {
element: ExcalidrawElement | ExcalidrawTextElement;
canvas: HTMLCanvasElement;
theme: RenderConfig["theme"];
canvasZoom: Zoom["value"];
scale: number;
canvasOffsetX: number;
canvasOffsetY: number;
boundTextElementVersion: number | null;
}
export const cappedElementCanvasSize = (
const cappedElementCanvasSize = (
element: NonDeletedExcalidrawElement,
zoom: Zoom,
): {
width: number;
height: number;
zoomValue: NormalizedZoomValue;
scale: number;
} => {
const sizelimit = 16777216; // 2^24
// these limits are ballpark, they depend on specific browsers and device.
// We've chosen lower limits to be safe. We might want to change these limits
// based on browser/device type, if we get reports of low quality rendering
// on zoom.
//
// ~ safari mobile canvas area limit
const AREA_LIMIT = 16777216;
// ~ safari width/height limit based on developer.mozilla.org.
const WIDTH_HEIGHT_LIMIT = 32767;
const padding = getCanvasPadding(element);
let zoomValue = zoom.value;
if (isLinearElement(element) || isFreeDrawElement(element)) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const elementWidth =
isLinearElement(element) || isFreeDrawElement(element)
? distance(x1, x2)
: element.width;
const elementHeight =
isLinearElement(element) || isFreeDrawElement(element)
? distance(y1, y2)
: element.height;
let width = distance(x1, x2) * window.devicePixelRatio + padding * 2;
let height = distance(y1, y2) * window.devicePixelRatio + padding * 2;
let width = elementWidth * window.devicePixelRatio + padding * 2;
let height = elementHeight * window.devicePixelRatio + padding * 2;
const size = width * height * zoomValue * zoomValue;
if (size > sizelimit) {
zoomValue = Math.sqrt(
sizelimit / (width * height),
) as NormalizedZoomValue;
width = distance(x1, x2) * window.devicePixelRatio + padding * 2;
height = distance(y1, y2) * window.devicePixelRatio + padding * 2;
}
width *= zoomValue;
height *= zoomValue;
return { width, height, zoomValue };
let scale: number = zoom.value;
// rescale to ensure width and height is within limits
if (
width * scale > WIDTH_HEIGHT_LIMIT ||
height * scale > WIDTH_HEIGHT_LIMIT
) {
scale = Math.min(WIDTH_HEIGHT_LIMIT / width, WIDTH_HEIGHT_LIMIT / height);
}
let width = element.width * window.devicePixelRatio + padding * 2;
let height = element.height * window.devicePixelRatio + padding * 2;
const size = width * height * zoomValue * zoomValue;
if (size > sizelimit) {
zoomValue = Math.sqrt(sizelimit / (width * height)) as NormalizedZoomValue;
width = element.width * window.devicePixelRatio + padding * 2;
height = element.height * window.devicePixelRatio + padding * 2;
// rescale to ensure canvas area is within limits
if (width * height * scale * scale > AREA_LIMIT) {
scale = Math.sqrt(AREA_LIMIT / (width * height));
}
width *= zoomValue;
height *= zoomValue;
return { width, height, zoomValue };
width = Math.floor(width * scale);
height = Math.floor(height * scale);
return { width, height, scale };
};
const generateElementCanvas = (
@@ -146,7 +156,7 @@ const generateElementCanvas = (
const context = canvas.getContext("2d")!;
const padding = getCanvasPadding(element);
const { width, height, zoomValue } = cappedElementCanvasSize(element, zoom);
const { width, height, scale } = cappedElementCanvasSize(element, zoom);
canvas.width = width;
canvas.height = height;
@@ -159,22 +169,22 @@ const generateElementCanvas = (
canvasOffsetX =
element.x > x1
? distance(element.x, x1) * window.devicePixelRatio * zoomValue
? distance(element.x, x1) * window.devicePixelRatio * scale
: 0;
canvasOffsetY =
element.y > y1
? distance(element.y, y1) * window.devicePixelRatio * zoomValue
? distance(element.y, y1) * window.devicePixelRatio * scale
: 0;
context.translate(canvasOffsetX, canvasOffsetY);
}
context.save();
context.translate(padding * zoomValue, padding * zoomValue);
context.translate(padding * scale, padding * scale);
context.scale(
window.devicePixelRatio * zoomValue,
window.devicePixelRatio * zoomValue,
window.devicePixelRatio * scale,
window.devicePixelRatio * scale,
);
const rc = rough.canvas(canvas);
@@ -191,7 +201,7 @@ const generateElementCanvas = (
element,
canvas,
theme: renderConfig.theme,
canvasZoom: zoomValue,
scale,
canvasOffsetX,
canvasOffsetY,
boundTextElementVersion: getBoundTextElement(element)?.version || null,
@@ -379,6 +389,16 @@ export const setShapeForElement = <T extends ExcalidrawElement>(
export const invalidateShapeForElement = (element: ExcalidrawElement) =>
shapeCache.delete(element);
function adjustRoughness(size: number, roughness: number): number {
if (size >= 50) {
return roughness;
}
const factor = 2 + (50 - size) / 10;
// console.log({ ret: roughness / factor, roughness, factor, size });
return roughness / factor;
}
export const generateRoughOptions = (
element: ExcalidrawElement,
continuousPath = false,
@@ -405,7 +425,10 @@ export const generateRoughOptions = (
// calculate them (and we don't want the fills to be modified)
fillWeight: element.strokeWidth / 2,
hachureGap: element.strokeWidth * 4,
roughness: element.roughness,
roughness: adjustRoughness(
Math.min(element.width, element.height),
element.roughness,
),
stroke: element.strokeColor,
preserveVertices: continuousPath,
};
@@ -457,11 +480,6 @@ const generateElementShape = (
// `null` indicates no rc shape applicable for this element type
// (= do not generate anything)
if (shape === undefined) {
const prevElementWithCanvas = elementWithCanvasCache.get(element);
if (prevElementWithCanvas?.canvas) {
prevElementWithCanvas.canvas.width = 0;
prevElementWithCanvas.canvas.height = 0;
}
elementWithCanvasCache.delete(element);
switch (element.type) {
@@ -710,7 +728,7 @@ const generateElementWithCanvas = (
const prevElementWithCanvas = elementWithCanvasCache.get(element);
const shouldRegenerateBecauseZoom =
prevElementWithCanvas &&
prevElementWithCanvas.canvasZoom !== zoom.value &&
prevElementWithCanvas.scale !== zoom.value &&
!renderConfig?.shouldCacheIgnoreZoom;
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
@@ -725,10 +743,7 @@ const generateElementWithCanvas = (
zoom,
renderConfig,
);
if (prevElementWithCanvas?.canvas) {
prevElementWithCanvas.canvas.width = 0;
prevElementWithCanvas.canvas.height = 0;
}
elementWithCanvasCache.set(element, elementWithCanvas);
return elementWithCanvas;
@@ -744,7 +759,7 @@ const drawElementFromCanvas = (
) => {
const element = elementWithCanvas.element;
const padding = getCanvasPadding(element);
const zoom = elementWithCanvas.canvasZoom;
const zoom = elementWithCanvas.scale;
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
// Free draw elements will otherwise "shuffle" as the min x and y change
@@ -771,10 +786,10 @@ const drawElementFromCanvas = (
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
tempCanvas.width =
maxDim * window.devicePixelRatio * zoom +
padding * elementWithCanvas.canvasZoom * 10;
padding * elementWithCanvas.scale * 10;
tempCanvas.height =
maxDim * window.devicePixelRatio * zoom +
padding * elementWithCanvas.canvasZoom * 10;
padding * elementWithCanvas.scale * 10;
const offsetX = (tempCanvas.width - elementWithCanvas.canvas!.width) / 2;
const offsetY = (tempCanvas.height - elementWithCanvas.canvas!.height) / 2;
@@ -855,11 +870,11 @@ const drawElementFromCanvas = (
context.drawImage(
elementWithCanvas.canvas!,
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
);
if (
+7 -25
View File
@@ -1,5 +1,5 @@
import { RoughCanvas } from "roughjs/bin/canvas";
import { RoughSVG } from "roughjs/bin/svg";
import { RoughCanvas } from "@excalidraw/roughjs/bin/canvas";
import { RoughSVG } from "@excalidraw/roughjs/bin/svg";
import oc from "open-color";
import { AppState, BinaryFiles, Point, Zoom } from "../types";
@@ -29,11 +29,7 @@ import {
} from "../scene/scrollbars";
import { getSelectedElements } from "../scene/selection";
import {
cappedElementCanvasSize,
renderElement,
renderElementToSvg,
} from "./renderElement";
import { renderElement, renderElementToSvg } from "./renderElement";
import { getClientColors } from "../clients";
import { LinearElementEditor } from "../element/linearElementEditor";
import {
@@ -411,21 +407,6 @@ export const _renderScene = ({
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
undefined;
const start = Date.now();
const showDebug = false; //!appState.shouldCacheIgnoreZoom && (appState.zoom.value < 0.5);
if (showDebug) {
console.log("start: renderElements");
}
console.log(
visibleElements.length,
appState.zoom.value,
Math.round(
visibleElements.reduce((acc, el) => {
const { width, height } = cappedElementCanvasSize(el, appState.zoom);
return acc + width * height;
}, 0),
),
);
visibleElements.forEach((element) => {
try {
renderElement(element, rc, context, renderConfig, appState);
@@ -445,9 +426,7 @@ export const _renderScene = ({
console.error(error);
}
});
if (showDebug) {
console.log(`finish: renderElements ${Date.now() - start}`);
}
if (editingLinearElement) {
renderLinearPointHandles(
context,
@@ -657,8 +636,10 @@ export const _renderScene = ({
}
context.restore();
}
// Reset zoom
context.restore();
// Paint remote pointers
for (const clientId in renderConfig.remotePointerViewportCoords) {
let { x, y } = renderConfig.remotePointerViewportCoords[clientId];
@@ -806,6 +787,7 @@ export const _renderScene = ({
});
context.restore();
}
context.restore();
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
};
+1 -1
View File
@@ -1,4 +1,4 @@
import rough from "roughjs/bin/rough";
import rough from "@excalidraw/roughjs/bin/rough";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element/bounds";
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
+1 -1
View File
@@ -16,7 +16,7 @@ import {
StrokeRoundness,
} from "./element/types";
import { SHAPES } from "./shapes";
import { Point as RoughPoint } from "roughjs/bin/geometry";
import { Point as RoughPoint } from "@excalidraw/roughjs/bin/geometry";
import { LinearElementEditor } from "./element/linearElementEditor";
import { SuggestedBinding } from "./element/binding";
import { ImportedDataState } from "./data/types";
-21
View File
@@ -135,24 +135,17 @@ export const throttleRAF = <T extends any[]>(
let timerId: number | null = null;
let lastArgs: T | null = null;
let lastArgsTrailing: T | null = null;
let watchdog: number | null = null;
const scheduleFunc = (args: T) => {
timerId = window.requestAnimationFrame(() => {
timerId = null;
//console.log("start render in animation frame");
fn(...args);
//console.log("render done in animation frame");
lastArgs = null;
if (lastArgsTrailing) {
//console.log("last args trailing", lastArgsTrailing);
lastArgs = lastArgsTrailing;
lastArgsTrailing = null;
scheduleFunc(lastArgs);
}
if (watchdog) {
clearTimeout(watchdog);
}
});
};
@@ -172,9 +165,6 @@ export const throttleRAF = <T extends any[]>(
if (timerId !== null) {
cancelAnimationFrame(timerId);
timerId = null;
if (watchdog) {
clearTimeout(watchdog);
}
}
if (lastArgs) {
fn(...(lastArgsTrailing || lastArgs));
@@ -186,19 +176,8 @@ export const throttleRAF = <T extends any[]>(
if (timerId !== null) {
cancelAnimationFrame(timerId);
timerId = null;
if (watchdog) {
clearTimeout(watchdog);
watchdog = null;
}
}
};
watchdog = window.setTimeout(() => {
console.log("watchdog", timerId);
if (timerId !== null) {
cancelAnimationFrame(timerId);
timerId = null;
}
}, 1000);
return ret;
};
+9
View File
@@ -1484,6 +1484,15 @@
resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65"
integrity sha512-rFIq8+A8WvkEzBsF++Rw6gzxE+hU3ZNkdg8foI+Upz2y/rOC/gUpWJaggPbCkoH3nlREVU59axQjZ1+F6ePRGg==
"@excalidraw/roughjs@4.5.3":
version "4.5.3"
resolved "https://registry.yarnpkg.com/@excalidraw/roughjs/-/roughjs-4.5.3.tgz#7d91897bfd1aaeb19a48d5272bd4776cb19f015b"
integrity sha512-X9HPfNjPLNHt+gN7t1LN7nN4h4o8M2EWuN/Y4YgG0ODsDZn51gRBeIPuhSo2hsDgLeTQHnkrmpPTm4XeeQVtoA==
dependencies:
path-data-parser "^0.1.0"
points-on-curve "^0.2.0"
points-on-path "^0.2.1"
"@firebase/analytics-types@0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz#d6716f9fa36a6e340bc0ecfe68af325aa6f60508"