Updates perfect-freehand, draw renderings

This commit is contained in:
Steve Ruiz 2021-09-17 17:37:40 +01:00
parent 39afd9a3f6
commit cdb7c74f8e
12 changed files with 221 additions and 117 deletions

View file

@ -832,6 +832,7 @@ export class Utils {
* @param tolerance The minimum line distance (also called epsilon). * @param tolerance The minimum line distance (also called epsilon).
* @returns Simplified array as [x, y, ...][] * @returns Simplified array as [x, y, ...][]
*/ */
static simplify(points: number[][], tolerance = 1): number[][] { static simplify(points: number[][], tolerance = 1): number[][] {
const len = points.length const len = points.length
const a = points[0] const a = points[0]

View file

@ -68,7 +68,7 @@
"@tldraw/core": "^0.0.94", "@tldraw/core": "^0.0.94",
"@tldraw/intersect": "^0.0.94", "@tldraw/intersect": "^0.0.94",
"@tldraw/vec": "^0.0.94", "@tldraw/vec": "^0.0.94",
"perfect-freehand": "^0.5.3", "perfect-freehand": "^1.0.4",
"react-hotkeys-hook": "^3.4.0", "react-hotkeys-hook": "^3.4.0",
"rko": "^0.5.25" "rko": "^0.5.25"
}, },

View file

@ -21,8 +21,7 @@ import {
intersectRayBounds, intersectRayBounds,
intersectRayEllipse, intersectRayEllipse,
} from '@tldraw/intersect' } from '@tldraw/intersect'
import { EASINGS } from '~state/utils'
const simplePathCache = new WeakMap<ArrowShape['handles'], string>()
export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() => ({ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() => ({
type: TLDrawShapeType.Arrow, type: TLDrawShapeType.Arrow,
@ -95,11 +94,15 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
let startArrowHead: { left: number[]; right: number[] } | undefined let startArrowHead: { left: number[]; right: number[] } | undefined
let endArrowHead: { left: number[]; right: number[] } | undefined let endArrowHead: { left: number[]; right: number[] } | undefined
const getRandom = Utils.rng(shape.id)
const easing = EASINGS[getRandom() > 0 ? 'easeInOutSine' : 'easeInOutCubic']
if (isStraightLine) { if (isStraightLine) {
const sw = strokeWidth * (isDraw ? 1.25 : 1.618) const sw = strokeWidth * (isDraw ? 1.25 : 2)
const path = isDraw const path = isDraw
? renderFreehandArrowShaft(shape) ? renderFreehandArrowShaft(shape, arrowDist, easing)
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point) : 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
@ -147,14 +150,14 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
} else { } else {
const circle = getCtp(shape) const circle = getCtp(shape)
const sw = strokeWidth * (isDraw ? 1.25 : 1.618) const sw = strokeWidth * (isDraw ? 1 : 2)
const path = isDraw
? renderCurvedFreehandArrowShaft(shape, circle)
: getArrowArcPath(start, end, circle, shape.bend)
const { center, radius, length } = getArrowArc(shape) const { center, radius, length } = getArrowArc(shape)
const path = isDraw
? renderCurvedFreehandArrowShaft(shape, circle, length, easing)
: getArrowArcPath(start, end, circle, shape.bend)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
length - 1, length - 1,
sw, sw,
@ -211,9 +214,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
) )
} }
const sw = strokeWidth * 1.618 const sw = strokeWidth * 2
const dots = getArcPoints(shape)
return ( return (
<SVGContainer ref={ref} {...events}> <SVGContainer ref={ref} {...events}>
@ -257,7 +258,11 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
}, },
shouldRender(prev, next) { shouldRender(prev, next) {
return next.handles !== prev.handles || next.style !== prev.style return (
next.decorations !== prev.decorations ||
next.handles !== prev.handles ||
next.style !== prev.style
)
}, },
getBounds(shape) { getBounds(shape) {
@ -603,23 +608,35 @@ function getBendPoint(handles: ArrowShape['handles'], bend: number) {
return point return point
} }
function renderFreehandArrowShaft(shape: ArrowShape) { function renderFreehandArrowShaft(
shape: ArrowShape,
length: number,
easing: (t: number) => number
) {
const { style, id } = shape const { style, id } = shape
const { start, end } = shape.handles const { start, end } = shape.handles
const getRandom = Utils.rng(id) const getRandom = Utils.rng(id)
const strokeWidth = +getShapeStyle(style).strokeWidth * 2 const strokeWidth = getShapeStyle(style).strokeWidth
const count = 4 + Math.floor((Math.abs(length) / 40) * (1 + getRandom() / 2))
const stroke = getStroke( const stroke = getStroke(
[...Vec.pointsBetween(start.point, end.point), end.point, end.point, end.point], [...Vec.pointsBetween(start.point, end.point, count, easing), end.point, end.point, end.point],
{ {
size: strokeWidth / 2, size: strokeWidth * 2,
thinning: 0.5 + getRandom() * 0.3, thinning: 0.618 + getRandom() * 0.2,
easing: (t) => t * t, start: shape.decorations?.start
end: shape.decorations?.end ? { cap: true } : { taper: strokeWidth * 20 }, ? { taper: length / 2 + 0.25 * Math.abs(getRandom()) }
start: shape.decorations?.start ? { cap: true } : { taper: strokeWidth * 20 }, : { cap: true },
end: shape.decorations?.end
? { taper: length / 2 + 0.25 * Math.abs(getRandom()) }
: { cap: true },
easing: EASINGS.easeOutQuad,
simulatePressure: true, simulatePressure: true,
smoothing: 0,
streamline: 0,
last: true, last: true,
} }
) )
@ -629,13 +646,18 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
return path return path
} }
function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) { function renderCurvedFreehandArrowShaft(
shape: ArrowShape,
circle: number[],
length: number,
easing: (t: number) => number
) {
const { style, id } = shape const { style, id } = shape
const { start, end } = shape.handles const { start, end } = shape.handles
const getRandom = Utils.rng(id) const getRandom = Utils.rng(id)
const strokeWidth = +getShapeStyle(style).strokeWidth * 2 const strokeWidth = getShapeStyle(style).strokeWidth
const center = [circle[0], circle[1]] const center = [circle[0], circle[1]]
const radius = circle[2] const radius = circle[2]
@ -646,20 +668,25 @@ function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
const points: number[][] = [] const points: number[][] = []
for (let i = 0; i < 21; i++) { const count = 8 + Math.floor((Math.abs(length) / 40) * 1 + getRandom() / 2)
const t = i / 20
for (let i = 0; i < count + 1; i++) {
const t = easing(i / count)
const angle = Utils.lerpAngles(startAngle, endAngle, t) const angle = Utils.lerpAngles(startAngle, endAngle, t)
points.push(Vec.round(Vec.nudgeAtAngle(center, angle, radius))) points.push(Vec.round(Vec.nudgeAtAngle(center, angle, radius)))
} }
const stroke = getStroke([...points, end.point, end.point, end.point], { const stroke = getStroke([...points, end.point, end.point, end.point], {
size: strokeWidth / 2, size: strokeWidth * 2,
thinning: 0.5 + getRandom() * 0.3, thinning: 0.618 + getRandom() * 0.2,
easing: (t) => t * t, start: shape.decorations?.start
end: shape.decorations?.end ? { cap: true } : { taper: strokeWidth * 20 }, ? { taper: length * (0.5 * Math.abs(getRandom())) }
start: shape.decorations?.start ? { cap: true } : { taper: strokeWidth * 20 }, : { cap: true },
end: shape.decorations?.end ? { taper: length * (0.5 * Math.abs(getRandom())) } : { cap: true },
easing: EASINGS.easeOutQuad,
simulatePressure: true, simulatePressure: true,
streamline: 0.01, streamline: 0,
smoothing: 0,
last: true, last: true,
}) })

View file

@ -295,19 +295,18 @@ function getFillPath(shape: DrawShape) {
function getDrawStrokePath(shape: DrawShape, isEditing: boolean) { function getDrawStrokePath(shape: DrawShape, isEditing: boolean) {
const styles = getShapeStyle(shape.style) const styles = getShapeStyle(shape.style)
if (shape.points.length < 2) { if (shape.points.length < 2) return ''
return ''
}
const options = shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings const options = shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings
const stroke = getStroke(shape.points.slice(2), { const stroke = getStroke(shape.points.slice(2), {
size: 1 + styles.strokeWidth * 2, size: 1 + styles.strokeWidth * 2,
thinning: 0.8, thinning: 0.7,
streamline: 0.7, streamline: 0.7,
smoothing: 0.6, smoothing: 0.5,
end: { taper: +styles.strokeWidth * 50 }, end: { taper: +styles.strokeWidth * 50 },
start: { taper: +styles.strokeWidth * 50 }, start: { taper: +styles.strokeWidth * 50 },
easing: (t) => Math.sin((t * Math.PI) / 2),
...options, ...options,
last: !isEditing, last: !isEditing,
}) })

View file

@ -9,6 +9,7 @@ import {
intersectLineSegmentEllipse, intersectLineSegmentEllipse,
intersectRayEllipse, intersectRayEllipse,
} from '@tldraw/intersect' } from '@tldraw/intersect'
import { EASINGS } from '~state/utils'
export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(() => ({ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(() => ({
type: TLDrawShapeType.Ellipse, type: TLDrawShapeType.Ellipse,
@ -44,7 +45,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
const ry = Math.max(0, radiusY - strokeWidth / 2) const ry = Math.max(0, radiusY - strokeWidth / 2)
if (style.dash === DashStyle.Draw) { if (style.dash === DashStyle.Draw) {
const path = renderPath(shape, this.getCenter(shape)) const path = getEllipsePath(shape, this.getCenter(shape))
return ( return (
<SVGContainer ref={ref} {...events}> <SVGContainer ref={ref} {...events}>
@ -302,7 +303,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
/* Helpers */ /* Helpers */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
function renderPath(shape: EllipseShape, boundsCenter: number[]) { function getEllipsePath(shape: EllipseShape, boundsCenter: number[]) {
const { const {
style, style,
id, id,
@ -316,42 +317,28 @@ function renderPath(shape: EllipseShape, boundsCenter: number[]) {
const strokeWidth = +getShapeStyle(style).strokeWidth const strokeWidth = +getShapeStyle(style).strokeWidth
const rx = radiusX + getRandom() * strokeWidth - strokeWidth / 2 const rx = radiusX + getRandom() * strokeWidth * 2
const ry = radiusY + getRandom() * strokeWidth - strokeWidth / 2 const ry = radiusY + getRandom() * strokeWidth * 2
const points: number[][] = [] const points: number[][] = []
const start = Math.PI + Math.PI * getRandom() const start = Math.PI + Math.PI * getRandom()
const extra = Math.abs(getRandom())
const overlap = Math.PI / 12 for (let i = 0; i < 32; i++) {
const t = EASINGS.easeInOutSine(i / 32)
for (let i = 2; i < 8; i++) { const rads = start * 2 + Math.PI * (2 + extra) * t
const rads = start + overlap * 2 * (i / 8)
const x = rx * Math.cos(rads) + center[0] const x = rx * Math.cos(rads) + center[0]
const y = ry * Math.sin(rads) + center[1] const y = ry * Math.sin(rads) + center[1]
points.push([x, y]) points.push([x, y, t + 0.5 + getRandom() / 2])
}
for (let i = 5; i < 32; i++) {
const t = i / 35
const rads = start + overlap * 2 + Math.PI * 2.5 * (t * t * t)
const x = rx * Math.cos(rads) + center[0]
const y = ry * Math.sin(rads) + center[1]
points.push([x, y])
}
for (let i = 0; i < 8; i++) {
const rads = start + overlap * 2 * (i / 4)
const x = rx * Math.cos(rads) + center[0]
const y = ry * Math.sin(rads) + center[1]
points.push([x, y])
} }
const stroke = getStroke(points, { const stroke = getStroke(points, {
size: 1 + strokeWidth, size: 1 + strokeWidth,
thinning: 0.6, thinning: 0.6,
easing: (t) => t * t * t * t, easing: EASINGS.easeInOutSine,
end: { taper: strokeWidth * 20 }, end: { taper: strokeWidth * 20 },
start: { taper: strokeWidth * 20 }, start: { taper: strokeWidth * 20 },
streamline: 0,
simulatePressure: false, simulatePressure: false,
}) })

View file

@ -5,6 +5,7 @@ import getStroke from 'perfect-freehand'
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles' import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types' import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types'
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared' import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
import { EASINGS } from '~state/utils'
const pathCache = new WeakMap<number[], string>([]) const pathCache = new WeakMap<number[], string>([])
@ -39,7 +40,7 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
this this
if (style.dash === DashStyle.Draw) { if (style.dash === DashStyle.Draw) {
const pathData = Utils.getFromCache(pathCache, shape.size, () => renderPath(shape)) const pathData = Utils.getFromCache(pathCache, shape.size, () => getRectanglePath(shape))
return ( return (
<SVGContainer ref={ref} {...events}> <SVGContainer ref={ref} {...events}>
@ -170,7 +171,7 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
/* Helpers */ /* Helpers */
/* -------------------------------------------------- */ /* -------------------------------------------------- */
function renderPath(shape: RectangleShape) { function getRectanglePath(shape: RectangleShape) {
const styles = getShapeStyle(shape.style) const styles = getShapeStyle(shape.style)
const getRandom = Utils.rng(shape.id) const getRandom = Utils.rng(shape.id)
@ -194,25 +195,32 @@ function renderPath(shape: RectangleShape) {
const br = Vec.add([w, h], offsets[2]) const br = Vec.add([w, h], offsets[2])
const bl = Vec.add([sw / 2, h], offsets[3]) const bl = Vec.add([sw / 2, h], offsets[3])
const px = Math.max(8, Math.floor(w / 20))
const py = Math.max(8, Math.floor(h / 20))
const rm = Math.floor(5 + getRandom() * 4)
const lines = Utils.shuffleArr( const lines = Utils.shuffleArr(
[ [
Vec.pointsBetween(tr, br), Vec.pointsBetween(tr, br, py, EASINGS.easeInOutCubic),
Vec.pointsBetween(br, bl), Vec.pointsBetween(br, bl, px, EASINGS.easeInOutCubic),
Vec.pointsBetween(bl, tl), Vec.pointsBetween(bl, tl, py, EASINGS.easeInOutCubic),
Vec.pointsBetween(tl, tr), Vec.pointsBetween(tl, tr, px, EASINGS.easeInOutCubic),
], ],
Math.floor(5 + getRandom() * 4) rm
) )
const stroke = getStroke([...lines.flat().slice(4), ...lines[0], ...lines[0].slice(4)], { const stroke = getStroke(
[...lines.flat(), ...lines[0], ...lines[1]].slice(4, Math.floor((rm % 2 === 0 ? px : py) / -2)),
{
size: 1 + styles.strokeWidth, size: 1 + styles.strokeWidth,
thinning: 0.618, thinning: 0.6,
easing: (t) => t * t * t * t, easing: EASINGS.easeOutSine,
end: { cap: true }, end: { cap: true },
start: { cap: true }, start: { cap: true },
simulatePressure: false, simulatePressure: false,
last: true, last: true,
}) }
)
return Utils.getSvgPathFromStroke(stroke) return Utils.getSvgPathFromStroke(stroke)
} }

View file

@ -142,6 +142,14 @@ export class TLDrawState extends StateManager<Data> {
/* -------------------- Internal -------------------- */ /* -------------------- Internal -------------------- */
onReady = () => { onReady = () => {
this.patchState({
appState: {
status: {
current: TLDrawStatus.Idle,
previous: TLDrawStatus.Idle,
},
},
})
this._onMount?.(this) this._onMount?.(this)
} }

View file

@ -0,0 +1,52 @@
import type { Easing } from '~types'
export const EASINGS: Record<Easing, (t: number) => number> = {
linear: (t) => t,
easeInQuad: (t) => t * t,
easeOutQuad: (t) => t * (2 - t),
easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
easeInCubic: (t) => t * t * t,
easeOutCubic: (t) => --t * t * t + 1,
easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
easeInQuart: (t) => t * t * t * t,
easeOutQuart: (t) => 1 - --t * t * t * t,
easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t),
easeInQuint: (t) => t * t * t * t * t,
easeOutQuint: (t) => 1 + --t * t * t * t * t,
easeInOutQuint: (t) => (t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t),
easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
easeInExpo: (t) => (t <= 0 ? 0 : Math.pow(2, 10 * t - 10)),
easeOutExpo: (t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t)),
easeInOutExpo: (t) =>
t <= 0
? 0
: t >= 1
? 1
: t < 0.5
? Math.pow(2, 20 * t - 10) / 2
: (2 - Math.pow(2, -20 * t + 10)) / 2,
}
export const EASING_STRINGS: Record<Easing, string> = {
linear: `(t) => t`,
easeInQuad: `(t) => t * t`,
easeOutQuad: `(t) => t * (2 - t)`,
easeInOutQuad: `(t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)`,
easeInCubic: `(t) => t * t * t`,
easeOutCubic: `(t) => --t * t * t + 1`,
easeInOutCubic: `(t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1`,
easeInQuart: `(t) => t * t * t * t`,
easeOutQuart: `(t) => 1 - --t * t * t * t`,
easeInOutQuart: `(t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t`,
easeInQuint: `(t) => t * t * t * t * t`,
easeOutQuint: `(t) => 1 + --t * t * t * t * t`,
easeInOutQuint: `(t) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t`,
easeInSine: `(t) => 1 - Math.cos((t * Math.PI) / 2)`,
easeOutSine: `(t) => Math.sin((t * Math.PI) / 2)`,
easeInOutSine: `(t) => -(Math.cos(Math.PI * t) - 1) / 2`,
easeInExpo: `(t) => (t <= 0 ? 0 : Math.pow(2, 10 * t - 10))`,
easeOutExpo: `(t) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t))`,
easeInOutExpo: `(t) => t <= 0 ? 0 : t >= 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2`,
}

View file

@ -307,3 +307,24 @@ export type ShapesWithProp<U> = MembersWithRequiredKey<
MappedByType<TLDrawShapeType, TLDrawShape>, MappedByType<TLDrawShapeType, TLDrawShape>,
U U
> >
export type Easing =
| 'linear'
| 'easeInQuad'
| 'easeOutQuad'
| 'easeInOutQuad'
| 'easeInCubic'
| 'easeOutCubic'
| 'easeInOutCubic'
| 'easeInQuart'
| 'easeOutQuart'
| 'easeInOutQuart'
| 'easeInQuint'
| 'easeOutQuint'
| 'easeInOutQuint'
| 'easeInSine'
| 'easeOutSine'
| 'easeInOutSine'
| 'easeInExpo'
| 'easeOutExpo'
| 'easeInOutExpo'

View file

@ -88,6 +88,11 @@ export class Vec {
return [A[0] * n, A[1] * n] return [A[0] * n, A[1] * n]
} }
/**
* Multiple two vectors.
* @param A
* @param B
*/
static mulV = (A: number[], B: number[]): number[] => { static mulV = (A: number[], B: number[]): number[] => {
return [A[0] * B[0], A[1] * B[1]] return [A[0] * B[0], A[1] * B[1]]
} }
@ -300,7 +305,7 @@ export class Vec {
* @param t scalar * @param t scalar
*/ */
static lrp = (A: number[], B: number[], t: number): number[] => { static lrp = (A: number[], B: number[], t: number): number[] => {
return Vec.add(A, Vec.mul(Vec.vec(A, B), t)) return Vec.add(A, Vec.mul(Vec.sub(B, A), t))
} }
/** /**
@ -501,18 +506,21 @@ export class Vec {
} }
/** /**
* Get a number of points between two points. * Get an array of points (with simulated pressure) between two points.
* @param a * @param A The first point.
* @param b * @param B The second point.
* @param steps * @param steps The number of points to return.
* @param ease An easing function to apply to the simulated pressure.
*/ */
static pointsBetween = (a: number[], b: number[], steps = 6): number[][] => { static pointsBetween = (
return Array.from(Array(steps)) A: number[],
.map((_, i) => { B: number[],
const t = i / steps steps = 6,
return t * t * t * t ease = (t: number) => t * t * t * t
}) ): number[][] => {
.map((t) => Vec.round([...Vec.lrp(a, b, t), (1 - t) / 2])) return Array.from(Array(steps)).map((_, i) =>
Vec.round([...Vec.lrp(A, B, i / steps), (1 - ease(i / steps)) / 2])
)
} }
} }

View file

@ -0,0 +1,15 @@
import { signOut } from 'next-auth/client'
import Head from 'next/head'
export default function SignOut(): JSX.Element {
return (
<>
<Head>
<title>tldraw</title>
</Head>
<div>
<button onClick={() => signOut()}>Sign Out</button>
</div>
</>
)
}

View file

@ -3626,15 +3626,6 @@
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0" "@testing-library/dom" "^8.0.0"
"@tldraw/core@^0.0.53":
version "0.0.53"
resolved "https://registry.yarnpkg.com/@tldraw/core/-/core-0.0.53.tgz#2db2b27df441169e452e0aa07570adca8b06b582"
integrity sha512-hxZIUR3Sm320tvGW5lWEKfw1QJhe6mJu7IrG5ka5G3slusqaY3cQY9EafFqH07yEXul2MU2RENIQus7fh+Gwcg==
dependencies:
deepmerge "^4.2.2"
ismobilejs "^1.1.1"
react-use-gesture "^9.1.3"
"@tootallnate/once@1": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@ -8144,11 +8135,6 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
ismobilejs@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ismobilejs/-/ismobilejs-1.1.1.tgz#c56ca0ae8e52b24ca0f22ba5ef3215a2ddbbaa0e"
integrity sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==
isobject@^2.0.0: isobject@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
@ -10744,13 +10730,10 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
perfect-freehand@^0.5.3: perfect-freehand@^1.0.4:
version "0.5.3" version "1.0.4"
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-0.5.3.tgz#41641d17aceb795db445ae84573396e3cce8878f" resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.4.tgz#1c5318164b10a74a4e6664b8519ff68799b8ab52"
integrity sha512-WbMEf78Chx1APwGNoKQf64fbUa12fCAXziKUf2BWoeZ2upsKu5OirCzLnIgjeZYkIB6jOoOtQawCb7CZZ+t/Aw== integrity sha512-feJV7C2LSiz6czFZuexYzxh8GHzaQ32bU4Vx+Y4xdCZYxnPFFyCoMVbVL3797zmk0v5rzGJkVfMQRUkLb/uZIg==
dependencies:
"@tldraw/core" "^0.0.53"
rko "^0.5.19"
performance-now@^2.1.0: performance-now@^2.1.0:
version "2.1.0" version "2.1.0"
@ -11213,11 +11196,6 @@ react-style-singleton@^2.1.0:
invariant "^2.2.4" invariant "^2.2.4"
tslib "^1.0.0" tslib "^1.0.0"
react-use-gesture@^9.1.3:
version "9.1.3"
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-9.1.3.tgz#92bd143e4f58e69bd424514a5bfccba2a1d62ec0"
integrity sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==
react@17.0.2, react@^17.0.2: react@17.0.2, react@^17.0.2:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@ -11681,7 +11659,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0" hash-base "^3.0.0"
inherits "^2.0.1" inherits "^2.0.1"
rko@^0.5.19, rko@^0.5.25: rko@^0.5.25:
version "0.5.25" version "0.5.25"
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.25.tgz#1095803900e3f912f6adf8a1c113b8227d3d88bf" resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.25.tgz#1095803900e3f912f6adf8a1c113b8227d3d88bf"
integrity sha512-HU6M3PxK3VEqrr6QZKAsqO98juQX24kEgJkKSdFJhw8U/DBUGAnU/fgyxNIaTw7TCI7vjIy/RzBEXf5I4sijKg== integrity sha512-HU6M3PxK3VEqrr6QZKAsqO98juQX24kEgJkKSdFJhw8U/DBUGAnU/fgyxNIaTw7TCI7vjIy/RzBEXf5I4sijKg==