Updates perfect-freehand, draw renderings
This commit is contained in:
parent
39afd9a3f6
commit
cdb7c74f8e
12 changed files with 221 additions and 117 deletions
|
@ -832,6 +832,7 @@ export class Utils {
|
|||
* @param tolerance The minimum line distance (also called epsilon).
|
||||
* @returns Simplified array as [x, y, ...][]
|
||||
*/
|
||||
|
||||
static simplify(points: number[][], tolerance = 1): number[][] {
|
||||
const len = points.length
|
||||
const a = points[0]
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
"@tldraw/core": "^0.0.94",
|
||||
"@tldraw/intersect": "^0.0.94",
|
||||
"@tldraw/vec": "^0.0.94",
|
||||
"perfect-freehand": "^0.5.3",
|
||||
"perfect-freehand": "^1.0.4",
|
||||
"react-hotkeys-hook": "^3.4.0",
|
||||
"rko": "^0.5.25"
|
||||
},
|
||||
|
|
|
@ -21,8 +21,7 @@ import {
|
|||
intersectRayBounds,
|
||||
intersectRayEllipse,
|
||||
} from '@tldraw/intersect'
|
||||
|
||||
const simplePathCache = new WeakMap<ArrowShape['handles'], string>()
|
||||
import { EASINGS } from '~state/utils'
|
||||
|
||||
export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Arrow,
|
||||
|
@ -95,11 +94,15 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
let startArrowHead: { 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) {
|
||||
const sw = strokeWidth * (isDraw ? 1.25 : 1.618)
|
||||
const sw = strokeWidth * (isDraw ? 1.25 : 2)
|
||||
|
||||
const path = isDraw
|
||||
? renderFreehandArrowShaft(shape)
|
||||
? renderFreehandArrowShaft(shape, arrowDist, easing)
|
||||
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
|
@ -147,14 +150,14 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
} else {
|
||||
const circle = getCtp(shape)
|
||||
|
||||
const sw = strokeWidth * (isDraw ? 1.25 : 1.618)
|
||||
|
||||
const path = isDraw
|
||||
? renderCurvedFreehandArrowShaft(shape, circle)
|
||||
: getArrowArcPath(start, end, circle, shape.bend)
|
||||
const sw = strokeWidth * (isDraw ? 1 : 2)
|
||||
|
||||
const { center, radius, length } = getArrowArc(shape)
|
||||
|
||||
const path = isDraw
|
||||
? renderCurvedFreehandArrowShaft(shape, circle, length, easing)
|
||||
: getArrowArcPath(start, end, circle, shape.bend)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
length - 1,
|
||||
sw,
|
||||
|
@ -211,9 +214,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
)
|
||||
}
|
||||
|
||||
const sw = strokeWidth * 1.618
|
||||
|
||||
const dots = getArcPoints(shape)
|
||||
const sw = strokeWidth * 2
|
||||
|
||||
return (
|
||||
<SVGContainer ref={ref} {...events}>
|
||||
|
@ -257,7 +258,11 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
},
|
||||
|
||||
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) {
|
||||
|
@ -603,23 +608,35 @@ function getBendPoint(handles: ArrowShape['handles'], bend: number) {
|
|||
return point
|
||||
}
|
||||
|
||||
function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||
function renderFreehandArrowShaft(
|
||||
shape: ArrowShape,
|
||||
length: number,
|
||||
easing: (t: number) => number
|
||||
) {
|
||||
const { style, id } = shape
|
||||
const { start, end } = shape.handles
|
||||
|
||||
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(
|
||||
[...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,
|
||||
thinning: 0.5 + getRandom() * 0.3,
|
||||
easing: (t) => t * t,
|
||||
end: shape.decorations?.end ? { cap: true } : { taper: strokeWidth * 20 },
|
||||
start: shape.decorations?.start ? { cap: true } : { taper: strokeWidth * 20 },
|
||||
size: strokeWidth * 2,
|
||||
thinning: 0.618 + getRandom() * 0.2,
|
||||
start: shape.decorations?.start
|
||||
? { taper: length / 2 + 0.25 * Math.abs(getRandom()) }
|
||||
: { cap: true },
|
||||
end: shape.decorations?.end
|
||||
? { taper: length / 2 + 0.25 * Math.abs(getRandom()) }
|
||||
: { cap: true },
|
||||
easing: EASINGS.easeOutQuad,
|
||||
simulatePressure: true,
|
||||
smoothing: 0,
|
||||
streamline: 0,
|
||||
last: true,
|
||||
}
|
||||
)
|
||||
|
@ -629,13 +646,18 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
|||
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 { start, end } = shape.handles
|
||||
|
||||
const getRandom = Utils.rng(id)
|
||||
|
||||
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
|
||||
const strokeWidth = getShapeStyle(style).strokeWidth
|
||||
|
||||
const center = [circle[0], circle[1]]
|
||||
const radius = circle[2]
|
||||
|
@ -646,20 +668,25 @@ function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
|
|||
|
||||
const points: number[][] = []
|
||||
|
||||
for (let i = 0; i < 21; i++) {
|
||||
const t = i / 20
|
||||
const count = 8 + Math.floor((Math.abs(length) / 40) * 1 + getRandom() / 2)
|
||||
|
||||
for (let i = 0; i < count + 1; i++) {
|
||||
const t = easing(i / count)
|
||||
const angle = Utils.lerpAngles(startAngle, endAngle, t)
|
||||
points.push(Vec.round(Vec.nudgeAtAngle(center, angle, radius)))
|
||||
}
|
||||
|
||||
const stroke = getStroke([...points, end.point, end.point, end.point], {
|
||||
size: strokeWidth / 2,
|
||||
thinning: 0.5 + getRandom() * 0.3,
|
||||
easing: (t) => t * t,
|
||||
end: shape.decorations?.end ? { cap: true } : { taper: strokeWidth * 20 },
|
||||
start: shape.decorations?.start ? { cap: true } : { taper: strokeWidth * 20 },
|
||||
size: strokeWidth * 2,
|
||||
thinning: 0.618 + getRandom() * 0.2,
|
||||
start: shape.decorations?.start
|
||||
? { taper: length * (0.5 * Math.abs(getRandom())) }
|
||||
: { cap: true },
|
||||
end: shape.decorations?.end ? { taper: length * (0.5 * Math.abs(getRandom())) } : { cap: true },
|
||||
easing: EASINGS.easeOutQuad,
|
||||
simulatePressure: true,
|
||||
streamline: 0.01,
|
||||
streamline: 0,
|
||||
smoothing: 0,
|
||||
last: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -295,19 +295,18 @@ function getFillPath(shape: DrawShape) {
|
|||
function getDrawStrokePath(shape: DrawShape, isEditing: boolean) {
|
||||
const styles = getShapeStyle(shape.style)
|
||||
|
||||
if (shape.points.length < 2) {
|
||||
return ''
|
||||
}
|
||||
if (shape.points.length < 2) return ''
|
||||
|
||||
const options = shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings
|
||||
|
||||
const stroke = getStroke(shape.points.slice(2), {
|
||||
size: 1 + styles.strokeWidth * 2,
|
||||
thinning: 0.8,
|
||||
thinning: 0.7,
|
||||
streamline: 0.7,
|
||||
smoothing: 0.6,
|
||||
smoothing: 0.5,
|
||||
end: { taper: +styles.strokeWidth * 50 },
|
||||
start: { taper: +styles.strokeWidth * 50 },
|
||||
easing: (t) => Math.sin((t * Math.PI) / 2),
|
||||
...options,
|
||||
last: !isEditing,
|
||||
})
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
intersectLineSegmentEllipse,
|
||||
intersectRayEllipse,
|
||||
} from '@tldraw/intersect'
|
||||
import { EASINGS } from '~state/utils'
|
||||
|
||||
export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Ellipse,
|
||||
|
@ -44,7 +45,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
|
|||
const ry = Math.max(0, radiusY - strokeWidth / 2)
|
||||
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
const path = renderPath(shape, this.getCenter(shape))
|
||||
const path = getEllipsePath(shape, this.getCenter(shape))
|
||||
|
||||
return (
|
||||
<SVGContainer ref={ref} {...events}>
|
||||
|
@ -302,7 +303,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
|
|||
/* Helpers */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
function renderPath(shape: EllipseShape, boundsCenter: number[]) {
|
||||
function getEllipsePath(shape: EllipseShape, boundsCenter: number[]) {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
|
@ -316,42 +317,28 @@ function renderPath(shape: EllipseShape, boundsCenter: number[]) {
|
|||
|
||||
const strokeWidth = +getShapeStyle(style).strokeWidth
|
||||
|
||||
const rx = radiusX + getRandom() * strokeWidth - strokeWidth / 2
|
||||
const ry = radiusY + getRandom() * strokeWidth - strokeWidth / 2
|
||||
const rx = radiusX + getRandom() * strokeWidth * 2
|
||||
const ry = radiusY + getRandom() * strokeWidth * 2
|
||||
|
||||
const points: number[][] = []
|
||||
const start = Math.PI + Math.PI * getRandom()
|
||||
const extra = Math.abs(getRandom())
|
||||
|
||||
const overlap = Math.PI / 12
|
||||
|
||||
for (let i = 2; i < 8; i++) {
|
||||
const rads = start + overlap * 2 * (i / 8)
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const t = EASINGS.easeInOutSine(i / 32)
|
||||
const rads = start * 2 + Math.PI * (2 + extra) * t
|
||||
const x = rx * Math.cos(rads) + center[0]
|
||||
const y = ry * Math.sin(rads) + center[1]
|
||||
points.push([x, y])
|
||||
}
|
||||
|
||||
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])
|
||||
points.push([x, y, t + 0.5 + getRandom() / 2])
|
||||
}
|
||||
|
||||
const stroke = getStroke(points, {
|
||||
size: 1 + strokeWidth,
|
||||
thinning: 0.6,
|
||||
easing: (t) => t * t * t * t,
|
||||
easing: EASINGS.easeInOutSine,
|
||||
end: { taper: strokeWidth * 20 },
|
||||
start: { taper: strokeWidth * 20 },
|
||||
streamline: 0,
|
||||
simulatePressure: false,
|
||||
})
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import getStroke from 'perfect-freehand'
|
|||
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types'
|
||||
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
||||
import { EASINGS } from '~state/utils'
|
||||
|
||||
const pathCache = new WeakMap<number[], string>([])
|
||||
|
||||
|
@ -39,7 +40,7 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
|
|||
this
|
||||
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
const pathData = Utils.getFromCache(pathCache, shape.size, () => renderPath(shape))
|
||||
const pathData = Utils.getFromCache(pathCache, shape.size, () => getRectanglePath(shape))
|
||||
|
||||
return (
|
||||
<SVGContainer ref={ref} {...events}>
|
||||
|
@ -170,7 +171,7 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
|
|||
/* Helpers */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
function renderPath(shape: RectangleShape) {
|
||||
function getRectanglePath(shape: RectangleShape) {
|
||||
const styles = getShapeStyle(shape.style)
|
||||
|
||||
const getRandom = Utils.rng(shape.id)
|
||||
|
@ -194,25 +195,32 @@ function renderPath(shape: RectangleShape) {
|
|||
const br = Vec.add([w, h], offsets[2])
|
||||
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(
|
||||
[
|
||||
Vec.pointsBetween(tr, br),
|
||||
Vec.pointsBetween(br, bl),
|
||||
Vec.pointsBetween(bl, tl),
|
||||
Vec.pointsBetween(tl, tr),
|
||||
Vec.pointsBetween(tr, br, py, EASINGS.easeInOutCubic),
|
||||
Vec.pointsBetween(br, bl, px, EASINGS.easeInOutCubic),
|
||||
Vec.pointsBetween(bl, tl, py, EASINGS.easeInOutCubic),
|
||||
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,
|
||||
thinning: 0.618,
|
||||
easing: (t) => t * t * t * t,
|
||||
thinning: 0.6,
|
||||
easing: EASINGS.easeOutSine,
|
||||
end: { cap: true },
|
||||
start: { cap: true },
|
||||
simulatePressure: false,
|
||||
last: true,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return Utils.getSvgPathFromStroke(stroke)
|
||||
}
|
||||
|
|
|
@ -142,6 +142,14 @@ export class TLDrawState extends StateManager<Data> {
|
|||
/* -------------------- Internal -------------------- */
|
||||
|
||||
onReady = () => {
|
||||
this.patchState({
|
||||
appState: {
|
||||
status: {
|
||||
current: TLDrawStatus.Idle,
|
||||
previous: TLDrawStatus.Idle,
|
||||
},
|
||||
},
|
||||
})
|
||||
this._onMount?.(this)
|
||||
}
|
||||
|
||||
|
|
52
packages/tldraw/src/state/utils.ts
Normal file
52
packages/tldraw/src/state/utils.ts
Normal 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`,
|
||||
}
|
|
@ -307,3 +307,24 @@ export type ShapesWithProp<U> = MembersWithRequiredKey<
|
|||
MappedByType<TLDrawShapeType, TLDrawShape>,
|
||||
U
|
||||
>
|
||||
|
||||
export type Easing =
|
||||
| 'linear'
|
||||
| 'easeInQuad'
|
||||
| 'easeOutQuad'
|
||||
| 'easeInOutQuad'
|
||||
| 'easeInCubic'
|
||||
| 'easeOutCubic'
|
||||
| 'easeInOutCubic'
|
||||
| 'easeInQuart'
|
||||
| 'easeOutQuart'
|
||||
| 'easeInOutQuart'
|
||||
| 'easeInQuint'
|
||||
| 'easeOutQuint'
|
||||
| 'easeInOutQuint'
|
||||
| 'easeInSine'
|
||||
| 'easeOutSine'
|
||||
| 'easeInOutSine'
|
||||
| 'easeInExpo'
|
||||
| 'easeOutExpo'
|
||||
| 'easeInOutExpo'
|
||||
|
|
|
@ -88,6 +88,11 @@ export class Vec {
|
|||
return [A[0] * n, A[1] * n]
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple two vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static mulV = (A: number[], B: number[]): number[] => {
|
||||
return [A[0] * B[0], A[1] * B[1]]
|
||||
}
|
||||
|
@ -300,7 +305,7 @@ export class Vec {
|
|||
* @param t scalar
|
||||
*/
|
||||
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.
|
||||
* @param a
|
||||
* @param b
|
||||
* @param steps
|
||||
* Get an array of points (with simulated pressure) between two points.
|
||||
* @param A The first point.
|
||||
* @param B The second point.
|
||||
* @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[][] => {
|
||||
return Array.from(Array(steps))
|
||||
.map((_, i) => {
|
||||
const t = i / steps
|
||||
return t * t * t * t
|
||||
})
|
||||
.map((t) => Vec.round([...Vec.lrp(a, b, t), (1 - t) / 2]))
|
||||
static pointsBetween = (
|
||||
A: number[],
|
||||
B: number[],
|
||||
steps = 6,
|
||||
ease = (t: number) => t * t * t * t
|
||||
): number[][] => {
|
||||
return Array.from(Array(steps)).map((_, i) =>
|
||||
Vec.round([...Vec.lrp(A, B, i / steps), (1 - ease(i / steps)) / 2])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
15
packages/www/pages/signout.tsx
Normal file
15
packages/www/pages/signout.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
32
yarn.lock
32
yarn.lock
|
@ -3626,15 +3626,6 @@
|
|||
"@babel/runtime" "^7.12.5"
|
||||
"@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":
|
||||
version "1.1.2"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
perfect-freehand@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-0.5.3.tgz#41641d17aceb795db445ae84573396e3cce8878f"
|
||||
integrity sha512-WbMEf78Chx1APwGNoKQf64fbUa12fCAXziKUf2BWoeZ2upsKu5OirCzLnIgjeZYkIB6jOoOtQawCb7CZZ+t/Aw==
|
||||
dependencies:
|
||||
"@tldraw/core" "^0.0.53"
|
||||
rko "^0.5.19"
|
||||
perfect-freehand@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.4.tgz#1c5318164b10a74a4e6664b8519ff68799b8ab52"
|
||||
integrity sha512-feJV7C2LSiz6czFZuexYzxh8GHzaQ32bU4Vx+Y4xdCZYxnPFFyCoMVbVL3797zmk0v5rzGJkVfMQRUkLb/uZIg==
|
||||
|
||||
performance-now@^2.1.0:
|
||||
version "2.1.0"
|
||||
|
@ -11213,11 +11196,6 @@ react-style-singleton@^2.1.0:
|
|||
invariant "^2.2.4"
|
||||
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:
|
||||
version "17.0.2"
|
||||
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"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rko@^0.5.19, rko@^0.5.25:
|
||||
rko@^0.5.25:
|
||||
version "0.5.25"
|
||||
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.25.tgz#1095803900e3f912f6adf8a1c113b8227d3d88bf"
|
||||
integrity sha512-HU6M3PxK3VEqrr6QZKAsqO98juQX24kEgJkKSdFJhw8U/DBUGAnU/fgyxNIaTw7TCI7vjIy/RzBEXf5I4sijKg==
|
||||
|
|
Loading…
Reference in a new issue