This commit is contained in:
dwelle 2024-09-16 10:46:37 +02:00
parent 508f16dc04
commit 0458834681
15 changed files with 228 additions and 29 deletions

View File

@ -1,5 +1,90 @@
import oc from "open-color";
import type { Merge } from "./utility-types";
import { clamp } from "../math/utils";
import tinycolor from "tinycolor2";
import { degreesToRadians } from "../math/angle";
import type { Degrees } from "../math/types";
function cssHueRotate(
red: number,
green: number,
blue: number,
degrees: Degrees,
): { r: number; g: number; b: number } {
// normalize
const r = red / 255;
const g = green / 255;
const b = blue / 255;
// Convert degrees to radians
const a = degreesToRadians(degrees);
const c = Math.cos(a);
const s = Math.sin(a);
// rotation matrix
const matrix = [
0.213 + c * 0.787 - s * 0.213,
0.715 - c * 0.715 - s * 0.715,
0.072 - c * 0.072 + s * 0.928,
0.213 - c * 0.213 + s * 0.143,
0.715 + c * 0.285 + s * 0.14,
0.072 - c * 0.072 - s * 0.283,
0.213 - c * 0.213 - s * 0.787,
0.715 - c * 0.715 + s * 0.715,
0.072 + c * 0.928 + s * 0.072,
];
// transform
const newR = r * matrix[0] + g * matrix[1] + b * matrix[2];
const newG = r * matrix[3] + g * matrix[4] + b * matrix[5];
const newB = r * matrix[6] + g * matrix[7] + b * matrix[8];
// clamp the values to [0, 1] range and convert back to [0, 255]
return {
r: Math.round(Math.max(0, Math.min(1, newR)) * 255),
g: Math.round(Math.max(0, Math.min(1, newG)) * 255),
b: Math.round(Math.max(0, Math.min(1, newB)) * 255),
};
}
const cssInvert = (
r: number,
g: number,
b: number,
percent: number,
): { r: number; g: number; b: number } => {
const p = clamp(percent, 0, 100) / 100;
// Function to invert a single color component
const invertComponent = (color: number): number => {
// Apply the invert formula
const inverted = color * (1 - p) + (255 - color) * p;
// Round to the nearest integer and clamp to [0, 255]
return Math.round(clamp(inverted, 0, 255));
};
// Calculate the inverted RGB components
const invertedR = invertComponent(r);
const invertedG = invertComponent(g);
const invertedB = invertComponent(b);
return { r: invertedR, g: invertedG, b: invertedB };
};
export const applyDarkModeFilter = (color: string) => {
let tc = tinycolor(color);
const _alpha = tc._a;
// order of operations matters
// (corresponds to "filter: invert(invertPercent) hue-rotate(hueDegrees)" in css)
tc = tinycolor(cssInvert(tc._r, tc._g, tc._b, 93));
tc = tinycolor(cssHueRotate(tc._r, tc._g, tc._b, 180 as Degrees));
tc.setAlpha(_alpha);
return tc.toHex8String();
};
// FIXME can't put to utils.ts rn because of circular dependency
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(

View File

@ -1700,6 +1700,7 @@ class App extends React.Component<AppProps, AppState> {
elementsPendingErasure: this.elementsPendingErasure,
pendingFlowchartNodes:
this.flowChartCreator.pendingNodes,
theme: this.state.theme,
}}
/>
{this.state.newElement && (
@ -1720,6 +1721,7 @@ class App extends React.Component<AppProps, AppState> {
elementsPendingErasure:
this.elementsPendingErasure,
pendingFlowchartNodes: null,
theme: this.state.theme,
}}
/>
)}
@ -2695,6 +2697,11 @@ class App extends React.Component<AppProps, AppState> {
activeTool: updateActiveTool(this.state, { type: "selection" }),
});
}
if (prevState.theme !== this.state.theme) {
this.scene
.getElementsIncludingDeleted()
.forEach((element) => ShapeCache.delete(element));
}
if (
this.state.activeTool.type === "eraser" &&
prevState.theme !== this.state.theme

View File

@ -124,7 +124,7 @@ body.excalidraw-cursor-resize * {
// recommends surface color of #121212, 93% yields #111111 for #FFF
canvas {
filter: var(--theme-filter);
// filter: var(--theme-filter);
}
}

View File

@ -189,7 +189,11 @@
}
}
$theme-filter: "invert(93%) hue-rotate(180deg)";
$theme-filter: "invert(93%) hue-rotate(180deg)"; // prod
// $theme-filter: "invert(93%)"; // prod
// $theme-filter: "hue-rotate(180deg)"; // prod
// $theme-filter: "hue-rotate(180deg) invert(93%)";
$right-sidebar-width: "302px";
:export {

View File

@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
import { CLASSES, isSafari, POINTER_BUTTON, THEME } from "../constants";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
@ -50,6 +50,7 @@ import {
originalContainerCache,
updateOriginalContainerCache,
} from "./containerCache";
import { applyDarkModeFilter } from "../colors";
const getTransform = (
width: number,
@ -273,10 +274,15 @@ export const textWysiwyg = ({
textAlign,
verticalAlign,
color: updatedTextElement.strokeColor,
// color:
// appState.theme === THEME.DARK
// ? applyDarkModeFilter(updatedTextElement.strokeColor)
// : updatedTextElement.strokeColor,
opacity: updatedTextElement.opacity / 100,
filter: "var(--theme-filter)",
maxHeight: `${editorMaxHeight}px`,
});
// console.log("...", updatedTextElement.strokeColor);
editable.scrollTop = 0;
// For some reason updating font attribute doesn't set font family
// hence updating font family explicitly for test environment

View File

@ -104,3 +104,13 @@ declare namespace jest {
toBeNonNaNNumber(): void;
}
}
declare namespace tinycolor {
interface Instance {
_r: number;
_g: number;
_b: number;
_a: number;
_ok: boolean;
}
}

View File

@ -63,6 +63,7 @@
"@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2",
"@tldraw/vec": "1.7.1",
"@types/tinycolor2": "1.4.6",
"browser-fs-access": "0.29.1",
"canvas-roundrect-polyfill": "0.0.1",
"clsx": "1.1.1",
@ -84,6 +85,7 @@
"pwacompat": "2.0.17",
"roughjs": "4.6.4",
"sass": "1.51.0",
"tinycolor2": "1.6.0",
"tunnel-rat": "0.1.2"
},
"devDependencies": {

View File

@ -3,6 +3,7 @@ import type { StaticCanvasAppState, AppState } from "../types";
import type { StaticCanvasRenderConfig } from "../scene/types";
import { THEME, THEME_FILTER } from "../constants";
import { applyDarkModeFilter } from "../colors";
export const fillCircle = (
context: CanvasRenderingContext2D,
@ -50,7 +51,7 @@ export const bootstrapCanvas = ({
context.scale(scale, scale);
if (isExporting && theme === THEME.DARK) {
context.filter = THEME_FILTER;
// context.filter = THEME_FILTER;
}
// Paint background
@ -64,7 +65,10 @@ export const bootstrapCanvas = ({
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
}
context.save();
context.fillStyle = viewBackgroundColor;
context.fillStyle =
theme === THEME.DARK
? applyDarkModeFilter(viewBackgroundColor)
: viewBackgroundColor;
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
context.restore();
} else {

View File

@ -61,6 +61,7 @@ import { ShapeCache } from "../scene/ShapeCache";
import { getVerticalOffset } from "../fonts";
import { isRightAngleRads } from "../../math";
import { getCornerRadius } from "../shapes";
import { applyDarkModeFilter } from "../colors";
// using a stronger invert (100% vs our regular 93%) and saturate
// as a temp hack to make images in dark theme look closer to original
@ -247,9 +248,9 @@ const generateElementCanvas = (
const rc = rough.canvas(canvas);
// in dark theme, revert the image color filter
if (shouldResetImageFilter(element, renderConfig, appState)) {
context.filter = IMAGE_INVERT_FILTER;
}
// if (shouldResetImageFilter(element, renderConfig, appState)) {
// context.filter = IMAGE_INVERT_FILTER;
// }
drawElementOnCanvas(element, rc, context, renderConfig, appState);
@ -403,7 +404,10 @@ const drawElementOnCanvas = (
case "freedraw": {
// Draw directly to canvas
context.save();
context.fillStyle = element.strokeColor;
context.fillStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor;
const path = getFreeDrawPath2D(element) as Path2D;
const fillShape = ShapeCache.get(element);
@ -412,7 +416,10 @@ const drawElementOnCanvas = (
rc.draw(fillShape);
}
context.fillStyle = element.strokeColor;
context.fillStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor;
context.fill(path);
context.restore();
@ -458,7 +465,10 @@ const drawElementOnCanvas = (
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
context.save();
context.font = getFontString(element);
context.fillStyle = element.strokeColor;
context.fillStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor;
context.textAlign = element.textAlign as CanvasTextAlign;
// Canvas does not support multiline text by default
@ -699,7 +709,10 @@ export const renderElement = (
context.fillStyle = "rgba(0, 0, 200, 0.04)";
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
context.strokeStyle = FRAME_STYLE.strokeColor;
context.strokeStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: FRAME_STYLE.strokeColor;
// TODO change later to only affect AI frames
if (isMagicFrameElement(element)) {

View File

@ -5,6 +5,7 @@ import {
MAX_DECIMALS_FOR_SVG_EXPORT,
MIME_TYPES,
SVG_NS,
THEME,
} from "../constants";
import { normalizeLink, toValidURL } from "../data/url";
import { getElementAbsoluteCoords } from "../element";
@ -37,6 +38,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
import { getVerticalOffset } from "../fonts";
import { getCornerRadius, isPathALoop } from "../shapes";
import { applyDarkModeFilter } from "../colors";
const roughSVGDrawWithPrecision = (
rsvg: RoughSVG,
@ -139,7 +141,7 @@ const renderElementToSvg = (
case "rectangle":
case "diamond":
case "ellipse": {
const shape = ShapeCache.generateElementShape(element, null);
const shape = ShapeCache.generateElementShape(element, renderConfig);
const node = roughSVGDrawWithPrecision(
rsvg,
shape,
@ -389,7 +391,12 @@ const renderElementToSvg = (
);
node.setAttribute("stroke", "none");
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
path.setAttribute("fill", element.strokeColor);
path.setAttribute(
"fill",
renderConfig.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
);
path.setAttribute("d", getFreeDrawSvgPath(element));
node.appendChild(path);
@ -526,7 +533,12 @@ const renderElementToSvg = (
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
rect.setAttribute("fill", "none");
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
rect.setAttribute(
"stroke",
renderConfig.theme === THEME.DARK
? applyDarkModeFilter(FRAME_STYLE.strokeColor)
: FRAME_STYLE.strokeColor,
);
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
addToRoot(rect, element);
@ -577,7 +589,12 @@ const renderElementToSvg = (
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
text.setAttribute("font-family", getFontFamilyString(element));
text.setAttribute("font-size", `${element.fontSize}px`);
text.setAttribute("fill", element.strokeColor);
text.setAttribute(
"fill",
renderConfig.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
);
text.setAttribute("text-anchor", textAnchor);
text.setAttribute("style", "white-space: pre;");
text.setAttribute("direction", direction);

View File

@ -13,7 +13,7 @@ import type {
import { generateFreeDrawShape } from "../renderer/renderElement";
import { isTransparent, assertNever } from "../utils";
import { simplify } from "points-on-curve";
import { ROUGHNESS } from "../constants";
import { ROUGHNESS, THEME } from "../constants";
import {
isElbowArrow,
isEmbeddableElement,
@ -22,7 +22,7 @@ import {
isLinearElement,
} from "../element/typeChecks";
import { canChangeRoundness } from "./comparisons";
import type { EmbedsValidationStatus } from "../types";
import type { AppState, EmbedsValidationStatus } from "../types";
import {
point,
pointDistance,
@ -30,6 +30,7 @@ import {
type LocalPoint,
} from "../../math";
import { getCornerRadius, isPathALoop } from "../shapes";
import { applyDarkModeFilter } from "../colors";
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
@ -61,6 +62,7 @@ function adjustRoughness(element: ExcalidrawElement): number {
export const generateRoughOptions = (
element: ExcalidrawElement,
continuousPath = false,
isDarkMode: boolean = false,
): Options => {
const options: Options = {
seed: element.seed,
@ -85,7 +87,9 @@ export const generateRoughOptions = (
fillWeight: element.strokeWidth / 2,
hachureGap: element.strokeWidth * 4,
roughness: adjustRoughness(element),
stroke: element.strokeColor,
stroke: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
preserveVertices:
continuousPath || element.roughness < ROUGHNESS.cartoonist,
};
@ -99,6 +103,8 @@ export const generateRoughOptions = (
options.fillStyle = element.fillStyle;
options.fill = isTransparent(element.backgroundColor)
? undefined
: isDarkMode
? applyDarkModeFilter(element.backgroundColor)
: element.backgroundColor;
if (element.type === "ellipse") {
options.curveFitting = 1;
@ -112,6 +118,8 @@ export const generateRoughOptions = (
options.fill =
element.backgroundColor === "transparent"
? undefined
: isDarkMode
? applyDarkModeFilter(element.backgroundColor)
: element.backgroundColor;
}
return options;
@ -165,6 +173,7 @@ const getArrowheadShapes = (
generator: RoughGenerator,
options: Options,
canvasBackgroundColor: string,
isDarkMode: boolean,
) => {
const arrowheadPoints = getArrowheadPoints(
element,
@ -192,10 +201,14 @@ const getArrowheadShapes = (
fill:
arrowhead === "circle_outline"
? canvasBackgroundColor
: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
fillStyle: "solid",
stroke: element.strokeColor,
stroke: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
roughness: Math.min(0.5, options.roughness || 0),
}),
];
@ -220,6 +233,8 @@ const getArrowheadShapes = (
fill:
arrowhead === "triangle_outline"
? canvasBackgroundColor
: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
fillStyle: "solid",
roughness: Math.min(1, options.roughness || 0),
@ -248,6 +263,8 @@ const getArrowheadShapes = (
fill:
arrowhead === "diamond_outline"
? canvasBackgroundColor
: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
fillStyle: "solid",
roughness: Math.min(1, options.roughness || 0),
@ -291,12 +308,15 @@ export const _generateElementShape = (
isExporting,
canvasBackgroundColor,
embedsValidationStatus,
theme,
}: {
isExporting: boolean;
canvasBackgroundColor: string;
embedsValidationStatus: EmbedsValidationStatus | null;
theme: AppState["theme"];
},
): Drawable | Drawable[] | null => {
const isDarkMode = theme === THEME.DARK;
switch (element.type) {
case "rectangle":
case "iframe":
@ -322,6 +342,7 @@ export const _generateElementShape = (
embedsValidationStatus,
),
true,
isDarkMode,
),
);
} else {
@ -337,6 +358,7 @@ export const _generateElementShape = (
embedsValidationStatus,
),
false,
isDarkMode,
),
);
}
@ -374,7 +396,7 @@ export const _generateElementShape = (
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
topY + horizontalRadius
}`,
generateRoughOptions(element, true),
generateRoughOptions(element, true, isDarkMode),
);
} else {
shape = generator.polygon(
@ -384,7 +406,7 @@ export const _generateElementShape = (
[bottomX, bottomY],
[leftX, leftY],
],
generateRoughOptions(element),
generateRoughOptions(element, undefined, isDarkMode),
);
}
return shape;
@ -395,14 +417,14 @@ export const _generateElementShape = (
element.height / 2,
element.width,
element.height,
generateRoughOptions(element),
generateRoughOptions(element, undefined, isDarkMode),
);
return shape;
}
case "line":
case "arrow": {
let shape: ElementShapes[typeof element.type];
const options = generateRoughOptions(element);
const options = generateRoughOptions(element, undefined, isDarkMode);
// points array can be empty in the beginning, so it is important to add
// initial position to it
@ -414,7 +436,7 @@ export const _generateElementShape = (
shape = [
generator.path(
generateElbowArrowShape(points, 16),
generateRoughOptions(element, true),
generateRoughOptions(element, true, isDarkMode),
),
];
} else if (!element.roundness) {
@ -446,6 +468,7 @@ export const _generateElementShape = (
generator,
options,
canvasBackgroundColor,
isDarkMode,
);
shape.push(...shapes);
}
@ -463,6 +486,7 @@ export const _generateElementShape = (
generator,
options,
canvasBackgroundColor,
isDarkMode,
);
shape.push(...shapes);
}
@ -477,7 +501,7 @@ export const _generateElementShape = (
// generate rough polygon to fill freedraw shape
const simplifiedPoints = simplify(element.points, 0.75);
shape = generator.curve(simplifiedPoints as [number, number][], {
...generateRoughOptions(element),
...generateRoughOptions(element, undefined, isDarkMode),
stroke: "none",
});
} else {

View File

@ -9,6 +9,7 @@ import { _generateElementShape } from "./Shape";
import type { ElementShape, ElementShapes } from "./types";
import { COLOR_PALETTE } from "../colors";
import type { AppState, EmbedsValidationStatus } from "../types";
import { THEME } from "..";
export class ShapeCache {
private static rg = new RoughGenerator();
@ -52,6 +53,7 @@ export class ShapeCache {
isExporting: boolean;
canvasBackgroundColor: AppState["viewBackgroundColor"];
embedsValidationStatus: EmbedsValidationStatus;
theme: AppState["theme"];
} | null,
) => {
// when exporting, always regenerated to guarantee the latest shape
@ -74,6 +76,7 @@ export class ShapeCache {
isExporting: false,
canvasBackgroundColor: COLOR_PALETTE.white,
embedsValidationStatus: null,
theme: THEME.LIGHT,
},
) as T["type"] extends keyof ElementShapes
? ElementShapes[T["type"]]

View File

@ -40,6 +40,7 @@ import { syncInvalidIndices } from "../fractionalIndex";
import { renderStaticScene } from "../renderer/staticScene";
import { Fonts } from "../fonts";
import type { Font } from "../fonts/ExcalidrawFont";
import { applyDarkModeFilter } from "../colors";
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
@ -214,6 +215,8 @@ export const exportToCanvas = async (
files,
});
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
renderStaticScene({
canvas,
rc: rough.canvas(canvas),
@ -233,7 +236,7 @@ export const exportToCanvas = async (
scrollY: -minY + exportPadding,
zoom: defaultAppState.zoom,
shouldCacheIgnoreZoom: false,
theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
theme,
},
renderConfig: {
canvasBackgroundColor: viewBackgroundColor,
@ -244,6 +247,7 @@ export const exportToCanvas = async (
embedsValidationStatus: new Map(),
elementsPendingErasure: new Set(),
pendingFlowchartNodes: null,
theme,
},
});
@ -330,7 +334,7 @@ export const exportToSvg = async (
svgRoot.setAttribute("width", `${width * exportScale}`);
svgRoot.setAttribute("height", `${height * exportScale}`);
if (exportWithDarkMode) {
svgRoot.setAttribute("filter", THEME_FILTER);
// svgRoot.setAttribute("filter", THEME_FILTER);
}
const offsetX = -minX + exportPadding;
@ -376,7 +380,12 @@ export const exportToSvg = async (
rect.setAttribute("y", "0");
rect.setAttribute("width", `${width}`);
rect.setAttribute("height", `${height}`);
rect.setAttribute("fill", viewBackgroundColor);
rect.setAttribute(
"fill",
appState.exportWithDarkMode
? applyDarkModeFilter(viewBackgroundColor)
: viewBackgroundColor,
);
svgRoot.appendChild(rect);
}
@ -384,6 +393,8 @@ export const exportToSvg = async (
const renderEmbeddables = opts?.renderEmbeddables ?? false;
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
renderSceneToSvg(
elementsForRender,
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
@ -405,6 +416,7 @@ export const exportToSvg = async (
.map((element) => [element.id, true]),
)
: new Map(),
theme,
},
);

View File

@ -35,6 +35,7 @@ export type StaticCanvasRenderConfig = {
embedsValidationStatus: EmbedsValidationStatus;
elementsPendingErasure: ElementsPendingErasure;
pendingFlowchartNodes: PendingExcalidrawElements | null;
theme: AppState["theme"];
};
export type SVGRenderConfig = {
@ -46,6 +47,7 @@ export type SVGRenderConfig = {
frameRendering: AppState["frameRendering"];
canvasBackgroundColor: AppState["viewBackgroundColor"];
embedsValidationStatus: EmbedsValidationStatus;
theme: AppState["theme"];
};
export type InteractiveCanvasRenderConfig = {

View File

@ -3299,6 +3299,11 @@
dependencies:
"@types/jest" "*"
"@types/tinycolor2@1.4.6":
version "1.4.6"
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz#670cbc0caf4e58dd61d1e3a6f26386e473087f06"
integrity sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==
"@types/trusted-types@^2.0.2":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
@ -9976,6 +9981,11 @@ tinybench@^2.8.0:
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
tinycolor2@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
tinypool@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"