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).
|
* @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]
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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>,
|
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'
|
||||||
|
|
|
@ -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])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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"
|
"@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==
|
||||||
|
|
Loading…
Reference in a new issue