kopia lustrzana https://github.com/Tldraw/Tldraw
295 wiersze
7.4 KiB
TypeScript
295 wiersze
7.4 KiB
TypeScript
import { Utils } from '@tldraw/core'
|
|
import { intersectCircleCircle, intersectCircleLineSegment } from '@tldraw/intersect'
|
|
import Vec from '@tldraw/vec'
|
|
import getStroke from 'perfect-freehand'
|
|
import { EASINGS } from '~constants'
|
|
import { getShapeStyle } from '../shared/shape-styles'
|
|
import type { ArrowShape, TldrawHandle } from '~types'
|
|
|
|
export function getArrowArcPath(
|
|
start: TldrawHandle,
|
|
end: TldrawHandle,
|
|
circle: number[],
|
|
bend: number
|
|
) {
|
|
return [
|
|
'M',
|
|
start.point[0],
|
|
start.point[1],
|
|
'A',
|
|
circle[2],
|
|
circle[2],
|
|
0,
|
|
0,
|
|
bend < 0 ? 0 : 1,
|
|
end.point[0],
|
|
end.point[1],
|
|
].join(' ')
|
|
}
|
|
|
|
export function getBendPoint(handles: ArrowShape['handles'], bend: number) {
|
|
const { start, end } = handles
|
|
|
|
const dist = Vec.dist(start.point, end.point)
|
|
|
|
const midPoint = Vec.med(start.point, end.point)
|
|
|
|
const bendDist = (dist / 2) * bend
|
|
|
|
const u = Vec.uni(Vec.vec(start.point, end.point))
|
|
|
|
const point = Vec.toFixed(
|
|
Math.abs(bendDist) < 10 ? midPoint : Vec.add(midPoint, Vec.mul(Vec.per(u), bendDist))
|
|
)
|
|
|
|
return point
|
|
}
|
|
|
|
export function renderFreehandArrowShaft(shape: ArrowShape) {
|
|
const { style, id } = shape
|
|
|
|
const { start, end } = shape.handles
|
|
|
|
const getRandom = Utils.rng(id)
|
|
|
|
const strokeWidth = getShapeStyle(style).strokeWidth
|
|
|
|
const startPoint = shape.decorations?.start
|
|
? Vec.nudge(start.point, end.point, strokeWidth)
|
|
: start.point
|
|
|
|
const endPoint = shape.decorations?.end
|
|
? Vec.nudge(end.point, start.point, strokeWidth)
|
|
: end.point
|
|
|
|
const stroke = getStroke([startPoint, endPoint], {
|
|
size: strokeWidth,
|
|
thinning: 0.618 + getRandom() * 0.2,
|
|
easing: EASINGS.easeOutQuad,
|
|
simulatePressure: true,
|
|
streamline: 0,
|
|
last: true,
|
|
})
|
|
|
|
const path = Utils.getSvgPathFromStroke(stroke)
|
|
|
|
return path
|
|
}
|
|
|
|
export 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
|
|
|
|
const center = [circle[0], circle[1]]
|
|
|
|
const radius = circle[2]
|
|
|
|
const startPoint = shape.decorations?.start
|
|
? Vec.rotWith(start.point, center, strokeWidth / length)
|
|
: start.point
|
|
|
|
const endPoint = shape.decorations?.end
|
|
? Vec.rotWith(end.point, center, -(strokeWidth / length))
|
|
: end.point
|
|
|
|
const startAngle = Vec.angle(center, startPoint)
|
|
|
|
const endAngle = Vec.angle(center, endPoint)
|
|
|
|
const points: number[][] = []
|
|
|
|
const count = 8 + Math.floor((Math.abs(length) / 20) * 1 + getRandom() / 2)
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const t = easing(i / count)
|
|
|
|
const angle = Utils.lerpAngles(startAngle, endAngle, t)
|
|
|
|
points.push(Vec.toFixed(Vec.nudgeAtAngle(center, angle, radius)))
|
|
}
|
|
|
|
const stroke = getStroke([startPoint, ...points, endPoint], {
|
|
size: 1 + strokeWidth,
|
|
thinning: 0.618 + getRandom() * 0.2,
|
|
easing: EASINGS.easeOutQuad,
|
|
simulatePressure: false,
|
|
streamline: 0,
|
|
last: true,
|
|
})
|
|
|
|
const path = Utils.getSvgPathFromStroke(stroke)
|
|
|
|
return path
|
|
}
|
|
|
|
export function getCtp(shape: ArrowShape) {
|
|
const { start, end, bend } = shape.handles
|
|
return Utils.circleFromThreePoints(start.point, end.point, bend.point)
|
|
}
|
|
|
|
export function getArrowArc(shape: ArrowShape) {
|
|
const { start, end, bend } = shape.handles
|
|
|
|
const [cx, cy, radius] = Utils.circleFromThreePoints(start.point, end.point, bend.point)
|
|
|
|
const center = [cx, cy]
|
|
|
|
const length = getArcLength(center, radius, start.point, end.point)
|
|
|
|
return { center, radius, length }
|
|
}
|
|
|
|
export function getCurvedArrowHeadPoints(
|
|
A: number[],
|
|
r1: number,
|
|
C: number[],
|
|
r2: number,
|
|
sweep: boolean
|
|
) {
|
|
const ints = intersectCircleCircle(A, r1 * 0.618, C, r2).points
|
|
|
|
if (!ints) {
|
|
console.warn('Could not find an intersection for the arrow head.')
|
|
return { left: A, right: A }
|
|
}
|
|
|
|
const int = sweep ? ints[0] : ints[1]
|
|
|
|
const left = int ? Vec.nudge(Vec.rotWith(int, A, Math.PI / 6), A, r1 * -0.382) : A
|
|
|
|
const right = int ? Vec.nudge(Vec.rotWith(int, A, -Math.PI / 6), A, r1 * -0.382) : A
|
|
|
|
return { left, right }
|
|
}
|
|
|
|
export function getStraightArrowHeadPoints(A: number[], B: number[], r: number) {
|
|
const ints = intersectCircleLineSegment(A, r, A, B).points
|
|
if (!ints) {
|
|
console.warn('Could not find an intersection for the arrow head.')
|
|
return { left: A, right: A }
|
|
}
|
|
|
|
const int = ints[0]
|
|
|
|
const left = int ? Vec.rotWith(int, A, Math.PI / 6) : A
|
|
|
|
const right = int ? Vec.rotWith(int, A, -Math.PI / 6) : A
|
|
|
|
return { left, right }
|
|
}
|
|
|
|
export function getCurvedArrowHeadPath(
|
|
A: number[],
|
|
r1: number,
|
|
C: number[],
|
|
r2: number,
|
|
sweep: boolean
|
|
) {
|
|
const { left, right } = getCurvedArrowHeadPoints(A, r1, C, r2, sweep)
|
|
|
|
return `M ${left} L ${A} ${right}`
|
|
}
|
|
|
|
export function getStraightArrowHeadPath(A: number[], B: number[], r: number) {
|
|
const { left, right } = getStraightArrowHeadPoints(A, B, r)
|
|
|
|
return `M ${left} L ${A} ${right}`
|
|
}
|
|
|
|
export function getArrowPath(shape: ArrowShape) {
|
|
const {
|
|
decorations,
|
|
handles: { start, end, bend: _bend },
|
|
style,
|
|
} = shape
|
|
|
|
const { strokeWidth } = getShapeStyle(style, false)
|
|
|
|
const arrowDist = Vec.dist(start.point, end.point)
|
|
|
|
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
|
|
|
|
const path: (string | number)[] = []
|
|
|
|
const isStraightLine = Vec.dist(_bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
|
|
|
|
if (isStraightLine) {
|
|
// Path (line segment)
|
|
path.push(`M ${start.point} L ${end.point}`)
|
|
|
|
// Start arrow head
|
|
if (decorations?.start) {
|
|
path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
|
|
}
|
|
|
|
// End arrow head
|
|
if (decorations?.end) {
|
|
path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
|
|
}
|
|
} else {
|
|
const { center, radius, length } = getArrowArc(shape)
|
|
|
|
// Path (arc)
|
|
path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)
|
|
|
|
// Start Arrow head
|
|
if (decorations?.start) {
|
|
path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
|
|
}
|
|
|
|
// End arrow head
|
|
if (decorations?.end) {
|
|
path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
|
|
}
|
|
}
|
|
|
|
return path.join(' ')
|
|
}
|
|
|
|
export function getArcPoints(shape: ArrowShape) {
|
|
const { start, bend, end } = shape.handles
|
|
|
|
if (Vec.dist2(bend.point, Vec.med(start.point, end.point)) > 4) {
|
|
const points: number[][] = []
|
|
|
|
// We're an arc, calculate points along the arc
|
|
const { center, radius } = getArrowArc(shape)
|
|
|
|
const startAngle = Vec.angle(center, start.point)
|
|
|
|
const endAngle = Vec.angle(center, end.point)
|
|
|
|
for (let i = 1 / 20; i < 1; i += 1 / 20) {
|
|
const angle = Utils.lerpAngles(startAngle, endAngle, i)
|
|
points.push(Vec.nudgeAtAngle(center, angle, radius))
|
|
}
|
|
|
|
return points
|
|
} else {
|
|
return [start.point, end.point]
|
|
}
|
|
}
|
|
|
|
export function isAngleBetween(a: number, b: number, c: number): boolean {
|
|
if (c === a || c === b) return true
|
|
const PI2 = Math.PI * 2
|
|
const AB = (b - a + PI2) % PI2
|
|
const AC = (c - a + PI2) % PI2
|
|
return AB <= Math.PI !== AC > AB
|
|
}
|
|
|
|
export function getArcLength(C: number[], r: number, A: number[], B: number[]): number {
|
|
const sweep = Utils.getSweep(C, A, B)
|
|
return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
|
|
}
|