canvas-rendering
Steve Ruiz 2021-05-12 23:08:53 +01:00
rodzic 3d52d9e9d2
commit 89d9ddcb1d
14 zmienionych plików z 531 dodań i 41 usunięć

Wyświetl plik

@ -1 +1,33 @@
export default function BoundsBg() {}
import state, { useSelector } from "state"
import styled from "styles"
export default function BoundsBg() {
const bounds = useSelector((state) => state.values.selectedBounds)
if (!bounds) return null
const { minX, minY, width, height } = bounds
return (
<StyledBoundsBg
x={minX}
y={minY}
width={width}
height={height}
onPointerDown={(e) => {
if (e.buttons !== 1) return
state.send("POINTED_BOUNDS", {
shiftKey: e.shiftKey,
optionKey: e.altKey,
metaKey: e.metaKey || e.ctrlKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
}}
/>
)
}
const StyledBoundsBg = styled("rect", {
fill: "$boundsBg",
})

Wyświetl plik

@ -1 +1,297 @@
export default function Bounds() {}
import state, { useSelector } from "state"
import { motion } from "framer-motion"
import styled from "styles"
export default function Bounds() {
const bounds = useSelector((state) => state.values.selectedBounds)
const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
const zoom = useSelector((state) => state.data.camera.zoom)
if (!bounds) return null
const { minX, minY, maxX, maxY, width, height } = bounds
const p = 4 / zoom
const cp = p * 2
return (
<g pointerEvents={isBrushing ? "none" : "all"}>
<StyledBounds
x={minX}
y={minY}
width={width}
height={height}
pointerEvents="none"
/>
<Corner
x={minX}
y={minY}
corner={0}
width={cp}
height={cp}
cursor="nwse-resize"
/>
<Corner
x={maxX}
y={minY}
corner={1}
width={cp}
height={cp}
cursor="nesw-resize"
/>
<Corner
x={maxX}
y={maxY}
corner={2}
width={cp}
height={cp}
cursor="nwse-resize"
/>
<Corner
x={minX}
y={maxY}
corner={3}
width={cp}
height={cp}
cursor="nesw-resize"
/>
<EdgeHorizontal
x={minX + p}
y={minY}
width={Math.max(0, width - p * 2)}
height={p}
onSelect={(e) => {
e.stopPropagation()
if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", {
edge: 0,
shiftKey: e.shiftKey,
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
document.body.style.cursor = "ns-resize"
}}
/>
<EdgeVertical
x={maxX}
y={minY + p}
width={p}
height={Math.max(0, height - p * 2)}
onSelect={(e) => {
e.stopPropagation()
if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", {
edge: 1,
shiftKey: e.shiftKey,
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
document.body.style.cursor = "ew-resize"
}}
/>
<EdgeHorizontal
x={minX + p}
y={maxY}
width={Math.max(0, width - p * 2)}
height={p}
onSelect={(e) => {
e.stopPropagation()
if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", {
edge: 2,
shiftKey: e.shiftKey,
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
document.body.style.cursor = "ns-resize"
}}
/>
<EdgeVertical
x={minX}
y={minY + p}
width={p}
height={Math.max(0, height - p * 2)}
onSelect={(e) => {
e.stopPropagation()
if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", {
edge: 3,
shiftKey: e.shiftKey,
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
document.body.style.cursor = "ew-resize"
}}
/>
</g>
)
}
function Corner({
x,
y,
width,
height,
cursor,
onHover,
corner,
}: {
x: number
y: number
width: number
height: number
cursor: string
corner: number
onHover?: () => void
}) {
const isTop = corner === 0 || corner === 1
const isLeft = corner === 0 || corner === 3
return (
<g>
<motion.rect
x={x + width * (isLeft ? -1.25 : -0.5)} // + width * 2 * transformOffset[0]}
y={y + width * (isTop ? -1.25 : -0.5)} // + height * 2 * transformOffset[1]}
width={width * 1.75}
height={height * 1.75}
onPanEnd={restoreCursor}
onTap={restoreCursor}
onPointerDown={(e) => {
e.stopPropagation()
if (e.buttons !== 1) return
state.send("POINTED_ROTATE_CORNER", {
corner,
shiftKey: e.shiftKey,
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
document.body.style.cursor = "grabbing"
}}
style={{ cursor: "grab" }}
fill="transparent"
/>
<StyledCorner
x={x + width * -0.5}
y={y + height * -0.5}
width={width}
height={height}
onPointerEnter={onHover}
onPointerDown={(e) => {
e.stopPropagation()
if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_CORNER", {
corner,
shiftKey: e.shiftKey,
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
document.body.style.cursor = "nesw-resize"
}}
onPanEnd={restoreCursor}
onTap={restoreCursor}
style={{ cursor }}
className="strokewidth-ui stroke-bounds fill-corner"
/>
</g>
)
}
function EdgeHorizontal({
x,
y,
width,
height,
onHover,
onSelect,
}: {
x: number
y: number
width: number
height: number
onHover?: () => void
onSelect?: (e: React.PointerEvent) => void
}) {
return (
<StyledEdge
x={x}
y={y - height / 2}
width={width}
height={height}
onPointerEnter={onHover}
onPointerDown={onSelect}
onPanEnd={restoreCursor}
onTap={restoreCursor}
style={{ cursor: "ns-resize" }}
direction="horizontal"
/>
)
}
function EdgeVertical({
x,
y,
width,
height,
onHover,
onSelect,
}: {
x: number
y: number
width: number
height: number
onHover?: () => void
onSelect?: (e: React.PointerEvent) => void
}) {
return (
<StyledEdge
x={x - width / 2}
y={y}
width={width}
height={height}
onPointerEnter={onHover}
onPointerDown={onSelect}
onPanEnd={restoreCursor}
onTap={restoreCursor}
direction="vertical"
/>
)
}
function restoreCursor() {
document.body.style.cursor = "default"
state.send("STOPPED_POINTING")
}
const StyledEdge = styled(motion.rect, {
stroke: "none",
fill: "none",
variant: {
direction: {
horizontal: { cursor: "ns-resize" },
vertical: { cursor: "ew-resize" },
},
},
})
const StyledCorner = styled(motion.rect, {
stroke: "$bounds",
fill: "#fff",
zStrokeWidth: 2,
})
const StyledBounds = styled("rect", {
fill: "none",
stroke: "$bounds",
zStrokeWidth: 2,
})

Wyświetl plik

@ -6,6 +6,8 @@ import useCamera from "hooks/useCamera"
import Page from "./page"
import Brush from "./brush"
import state from "state"
import Bounds from "./bounds"
import BoundsBg from "./bounds-bg"
export default function Canvas() {
const rCanvas = useRef<SVGSVGElement>(null)
@ -37,7 +39,9 @@ export default function Canvas() {
onPointerUp={handlePointerUp}
>
<MainGroup ref={rGroup}>
<BoundsBg />
<Page />
<Bounds />
<Brush />
</MainGroup>
</MainSVG>

Wyświetl plik

@ -1,19 +1,8 @@
import React, { useCallback, useRef } from "react"
import React, { useCallback, useRef, memo } from "react"
import state, { useSelector } from "state"
import styled from "styles"
import { getPointerEventInfo } from "utils/utils"
import { memo } from "react"
import Shapes from "lib/shapes"
/*
Gets the shape from the current page's shapes, using the
provided ID. Depending on the shape's type, return the
component for that type.
This component takes an SVG shape as its children. It handles
events for the shape as well as provides indicators for hover
and selected status
*/
import shapes from "lib/shapes"
import styled from "styles"
function Shape({ id }: { id: string }) {
const rGroup = useRef<SVGGElement>(null)
@ -66,7 +55,7 @@ function Shape({ id }: { id: string }) {
onPointerLeave={handlePointerLeave}
>
<defs>
{Shapes[shape.type] ? Shapes[shape.type].render(shape) : null}
{shapes[shape.type] ? shapes[shape.type].render(shape) : null}
</defs>
<HoverIndicator as="use" xlinkHref={"#" + id} />
<use xlinkHref={"#" + id} {...shape.style} />
@ -78,7 +67,7 @@ function Shape({ id }: { id: string }) {
const Indicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "max(1, calc(2 / var(--camera-zoom)))",
zStrokeWidth: 1,
pointerEvents: "none",
strokeLineCap: "round",
strokeLinejoin: "round",
@ -87,7 +76,7 @@ const Indicator = styled("path", {
const HoverIndicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "max(1, calc(8 / var(--camera-zoom)))",
zStrokeWidth: 8,
pointerEvents: "all",
strokeLinecap: "round",
strokeLinejoin: "round",

Wyświetl plik

@ -1,6 +1,7 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, CircleShape, ShapeType } from "types"
import { boundsCache } from "./index"
const Circle: BaseLibShape<ShapeType.Circle> = {
create(props): CircleShape {
@ -23,19 +24,26 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
},
getBounds(shape) {
if (boundsCache.has(shape)) {
return boundsCache.get(shape)
}
const {
point: [cx, cy],
point: [x, y],
radius,
} = shape
return {
minX: cx,
maxX: cx + radius * 2,
minY: cy,
maxY: cy + radius * 2,
const bounds = {
minX: x,
maxX: x + radius * 2,
minY: y,
maxY: y + radius * 2,
width: radius * 2,
height: radius * 2,
}
boundsCache.set(shape, bounds)
return bounds
},
hitTest(shape, test) {

Wyświetl plik

@ -1,6 +1,7 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, DotShape, ShapeType } from "types"
import { boundsCache } from "./index"
const Dot: BaseLibShape<ShapeType.Dot> = {
create(props): DotShape {
@ -22,18 +23,25 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
},
getBounds(shape) {
if (boundsCache.has(shape)) {
return boundsCache.get(shape)
}
const {
point: [cx, cy],
point: [x, y],
} = shape
return {
minX: cx,
maxX: cx + 4,
minY: cy,
maxY: cy + 4,
width: 4,
height: 4,
const bounds = {
minX: x,
maxX: x + 8,
minY: y,
maxY: y + 8,
width: 8,
height: 8,
}
boundsCache.set(shape, bounds)
return bounds
},
hitTest(shape, test) {

Wyświetl plik

@ -3,11 +3,15 @@ import Dot from "./dot"
import Polyline from "./polyline"
import Rectangle from "./rectangle"
import { ShapeType } from "types"
import { Bounds, Shape, ShapeType } from "types"
export default {
export const boundsCache = new WeakMap<Shape, Bounds>([])
const shapes = {
[ShapeType.Circle]: Circle,
[ShapeType.Dot]: Dot,
[ShapeType.Polyline]: Polyline,
[ShapeType.Rectangle]: Rectangle,
}
export default shapes

Wyświetl plik

@ -1,6 +1,7 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, PolylineShape, ShapeType } from "types"
import { boundsCache } from "./index"
const Polyline: BaseLibShape<ShapeType.Polyline> = {
create(props): PolylineShape {
@ -23,6 +24,10 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
},
getBounds(shape) {
if (boundsCache.has(shape)) {
return boundsCache.get(shape)
}
let minX = 0
let minY = 0
let maxX = 0
@ -35,7 +40,7 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
maxY = Math.max(y, maxY)
}
return {
const bounds = {
minX: minX + shape.point[0],
minY: minY + shape.point[1],
maxX: maxX + shape.point[0],
@ -43,6 +48,9 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
width: maxX - minX,
height: maxY - minY,
}
boundsCache.set(shape, bounds)
return bounds
},
hitTest(shape) {

Wyświetl plik

@ -1,6 +1,7 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, RectangleShape, ShapeType } from "types"
import { boundsCache } from "./index"
const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
create(props): RectangleShape {
@ -23,12 +24,16 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
},
getBounds(shape) {
if (boundsCache.has(shape)) {
return boundsCache.get(shape)
}
const {
point: [x, y],
size: [width, height],
} = shape
return {
const bounds = {
minX: x,
maxX: x + width,
minY: y,
@ -36,6 +41,9 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
width,
height,
}
boundsCache.set(shape, bounds)
return bounds
},
hitTest(shape) {

Wyświetl plik

@ -11,6 +11,7 @@
"@state-designer/react": "^1.7.1",
"@stitches/react": "^0.1.9",
"@types/uuid": "^8.3.0",
"framer-motion": "^4.1.16",
"next": "10.2.0",
"perfect-freehand": "^0.4.7",
"react": "17.0.2",

Wyświetl plik

@ -1,8 +1,9 @@
import { createSelectorHook, createState } from "@state-designer/react"
import { clamp, screenToWorld } from "utils/utils"
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
import * as vec from "utils/vec"
import { Data } from "types"
import { Bounds, Data, Shape, ShapeType } from "types"
import { defaultDocument } from "./data"
import Shapes from "lib/shapes"
import * as Sessions from "./sessions"
const initialData: Data = {
@ -131,6 +132,42 @@ const state = createState({
selectedIds(data) {
return new Set(data.selectedIds)
},
selectedBounds(data) {
const {
selectedIds,
currentPageId,
document: { pages },
} = data
return getCommonBounds(
...Array.from(selectedIds.values())
.map((id) => {
const shape = pages[currentPageId].shapes[id]
switch (shape.type) {
case ShapeType.Dot: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Circle: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Line: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Polyline: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Rectangle: {
return Shapes[shape.type].getBounds(shape)
}
default: {
return null
}
}
})
.filter(Boolean)
)
},
},
})

Wyświetl plik

@ -10,6 +10,8 @@ const { styled, global, css, theme, getCssString } = createCss({
brushStroke: "rgba(0,0,0,.5)",
hint: "rgba(66, 133, 244, 0.200)",
selected: "rgba(66, 133, 244, 1.000)",
bounds: "rgba(65, 132, 244, 1.000)",
boundsBg: "rgba(65, 132, 244, 0.100)",
},
space: {},
fontSizes: {
@ -33,6 +35,11 @@ const { styled, global, css, theme, getCssString } = createCss({
zIndices: {},
transitions: {},
},
utils: {
zStrokeWidth: () => (value: number) => ({
strokeWidth: `calc(${value}px / var(--camera-zoom))`,
}),
},
})
const light = theme({})

Wyświetl plik

@ -1,4 +1,4 @@
import { Data } from "types"
import { Data, Bounds } from "types"
import * as svg from "./svg"
import * as vec from "./vec"
@ -6,6 +6,39 @@ export function screenToWorld(point: number[], data: Data) {
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
}
/**
* Get a bounding box that includes two bounding boxes.
* @param a Bounding box
* @param b Bounding box
* @returns
*/
export function getExpandedBounds(a: Bounds, b: Bounds) {
const minX = Math.min(a.minX, b.minX),
minY = Math.min(a.minY, b.minY),
maxX = Math.max(a.maxX, b.maxX),
maxY = Math.max(a.maxY, b.maxY),
width = Math.abs(maxX - minX),
height = Math.abs(maxY - minY)
return { minX, minY, maxX, maxY, width, height }
}
/**
* Get the common bounds of a group of bounds.
* @returns
*/
export function getCommonBounds(...b: Bounds[]) {
if (b.length < 2) return b[0]
let bounds = b[0]
for (let i = 1; i < b.length; i++) {
bounds = getExpandedBounds(bounds, b[i])
}
return bounds
}
export function getBoundsFromPoints(a: number[], b: number[]) {
const minX = Math.min(a[0], b[0])
const maxX = Math.max(a[0], b[0])

Wyświetl plik

@ -939,6 +939,18 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@emotion/is-prop-valid@^0.8.2":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
dependencies:
"@emotion/memoize" "0.7.4"
"@emotion/memoize@0.7.4":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@hapi/accept@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10"
@ -3402,6 +3414,26 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
framer-motion@^4.1.16:
version "4.1.16"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.16.tgz#dc715334847d0a146acf47f61019222d0d1c46c9"
integrity sha512-sEc3UI3oncwE+RUzdd86TxbmpEaX/Ki/T0AmFYSsbxEqGZ3feLvzGL7BJlkhERIyyuAC9+OzI4BnhJM0GSUAMA==
dependencies:
framesync "5.3.0"
hey-listen "^1.0.8"
popmotion "9.3.6"
style-value-types "4.1.4"
tslib "^2.1.0"
optionalDependencies:
"@emotion/is-prop-valid" "^0.8.2"
framesync@5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.3.0.tgz#0ecfc955e8f5a6ddc8fdb0cc024070947e1a0d9b"
integrity sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==
dependencies:
tslib "^2.1.0"
fs-extra@8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
@ -3652,6 +3684,11 @@ he@1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -5622,6 +5659,16 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
popmotion@9.3.6:
version "9.3.6"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.6.tgz#b5236fa28f242aff3871b9e23721f093133248d1"
integrity sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==
dependencies:
framesync "5.3.0"
hey-listen "^1.0.8"
style-value-types "4.1.4"
tslib "^2.1.0"
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -6734,6 +6781,14 @@ strip-json-comments@^3.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-value-types@4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.1.4.tgz#80f37cb4fb024d6394087403dfb275e8bb627e75"
integrity sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==
dependencies:
hey-listen "^1.0.8"
tslib "^2.1.0"
styled-jsx@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"
@ -7051,7 +7106,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3:
tslib@^2.0.3, tslib@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==