Improves arrowheads, curved arrows

This commit is contained in:
Steve Ruiz 2021-07-08 13:15:23 +01:00
parent a5c07c3efd
commit 64fa2a19b1
2 changed files with 128 additions and 119 deletions

View file

@ -12,6 +12,8 @@ import {
isAngleBetween, isAngleBetween,
getPerfectDashProps, getPerfectDashProps,
clampToRotationToSegments, clampToRotationToSegments,
lerpAngles,
clamp,
} from 'utils' } from 'utils'
import { import {
ArrowShape, ArrowShape,
@ -106,24 +108,24 @@ const arrow = registerShapeUtils<ArrowShape>({
const isStraightLine = const isStraightLine =
vec.dist(_bend.point, vec.round(vec.med(start.point, end.point))) < 1 vec.dist(_bend.point, vec.round(vec.med(start.point, end.point))) < 1
const isDraw = shape.style.dash === DashStyle.Draw
const styles = getShapeStyle(style) const styles = getShapeStyle(style)
const strokeWidth = +styles.strokeWidth const strokeWidth = +styles.strokeWidth
const sw = strokeWidth * 1.618
const arrowDist = vec.dist(start.point, end.point) const arrowDist = vec.dist(start.point, end.point)
const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8)
let shaftPath: JSX.Element let shaftPath: JSX.Element
let startAngle: number let insetStart: number[]
let endAngle: number let insetEnd: number[]
if (isStraightLine) { if (isStraightLine) {
const straight_sw = const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
strokeWidth * (style.dash === DashStyle.Draw ? 0.618 : 1.618)
const path = const path = isDraw
shape.style.dash === DashStyle.Draw
? renderFreehandArrowShaft(shape) ? renderFreehandArrowShaft(shape)
: 'M' + vec.round(start.point) + 'L' + vec.round(end.point) : 'M' + vec.round(start.point) + 'L' + vec.round(end.point)
@ -134,9 +136,8 @@ const arrow = registerShapeUtils<ArrowShape>({
2 2
) )
startAngle = Math.PI insetStart = vec.nudge(start.point, end.point, arrowHeadlength)
insetEnd = vec.nudge(end.point, start.point, arrowHeadlength)
endAngle = 0
// Straight arrow path // Straight arrow path
shaftPath = ( shaftPath = (
@ -154,7 +155,7 @@ const arrow = registerShapeUtils<ArrowShape>({
d={path} d={path}
fill={styles.stroke} fill={styles.stroke}
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={straight_sw} strokeWidth={sw}
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset} strokeDashoffset={strokeDashoffset}
strokeLinecap="round" strokeLinecap="round"
@ -164,29 +165,34 @@ const arrow = registerShapeUtils<ArrowShape>({
} else { } else {
const circle = getCtp(shape) const circle = getCtp(shape)
const path = getArrowArcPath(start, end, circle, bend) const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( const path = isDraw
getArcLength( ? renderCurvedFreehandArrowShaft(shape, circle)
: getArrowArcPath(start, end, circle, bend)
const arcLength = getArcLength(
[circle[0], circle[1]], [circle[0], circle[1]],
circle[2], circle[2],
start.point, start.point,
end.point end.point
) - 1, )
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
arcLength - 1,
sw, sw,
shape.style.dash, shape.style.dash,
2 2
) )
startAngle = const center = [circle[0], circle[1]]
vec.angle([circle[0], circle[1]], start.point) - const radius = circle[2]
vec.angle(end.point, start.point) + const sa = vec.angle(center, start.point)
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98) const ea = vec.angle(center, end.point)
const t = arrowHeadlength / Math.abs(arcLength)
endAngle = insetStart = vec.nudgeAtAngle(center, lerpAngles(sa, ea, t), radius)
vec.angle([circle[0], circle[1]], end.point) - insetEnd = vec.nudgeAtAngle(center, lerpAngles(ea, sa, t), radius)
vec.angle(start.point, end.point) +
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
// Curved arrow path // Curved arrow path
shaftPath = ( shaftPath = (
@ -202,7 +208,7 @@ const arrow = registerShapeUtils<ArrowShape>({
/> />
<path <path
d={path} d={path}
fill="none" fill={isDraw ? styles.stroke : 'none'}
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={sw} strokeWidth={sw}
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
@ -218,20 +224,20 @@ const arrow = registerShapeUtils<ArrowShape>({
{shaftPath} {shaftPath}
{shape.decorations.start === Decoration.Arrow && ( {shape.decorations.start === Decoration.Arrow && (
<path <path
d={getArrowHeadPath(shape, start.point, startAngle)} d={getArrowHeadPath(shape, start.point, insetStart)}
fill="none" fill="none"
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={sw} strokeWidth={strokeWidth * 1.618}
strokeDashoffset="none" strokeDashoffset="none"
strokeDasharray="none" strokeDasharray="none"
/> />
)} )}
{shape.decorations.end === Decoration.Arrow && ( {shape.decorations.end === Decoration.Arrow && (
<path <path
d={getArrowHeadPath(shape, end.point, endAngle)} d={getArrowHeadPath(shape, end.point, insetEnd)}
fill="none" fill="none"
stroke={styles.stroke} stroke={styles.stroke}
strokeWidth={sw} strokeWidth={strokeWidth * 1.618}
strokeDashoffset="none" strokeDashoffset="none"
strokeDasharray="none" strokeDasharray="none"
/> />
@ -434,23 +440,22 @@ const arrow = registerShapeUtils<ArrowShape>({
const ap = vec.add(midPoint, vec.mul(vec.per(u), distance / 2)) const ap = vec.add(midPoint, vec.mul(vec.per(u), distance / 2))
const bp = vec.sub(midPoint, vec.mul(vec.per(u), distance / 2)) const bp = vec.sub(midPoint, vec.mul(vec.per(u), distance / 2))
const bendPoint = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
// Find the distance between the midpoint and the nearest point on the // Find the distance between the midpoint and the nearest point on the
// line segment to the bend handle's dragged point // line segment to the bend handle's dragged point
const bendDist = vec.dist( const bendDist = vec.dist(midPoint, bendPoint)
midPoint,
vec.round(vec.nearestPointOnLineSegment(ap, bp, bend.point, true))
)
// The shape's "bend" is the ratio of the bend to the distance between // The shape's "bend" is the ratio of the bend to the distance between
// the start and end points. If the bend is below a certain amount, the // the start and end points. If the bend is below a certain amount, the
// bend should be zero. // bend should be zero.
shape.bend = bendDist / (distance / 2) shape.bend = clamp(bendDist / (distance / 2), -0.99, 0.99)
// If the point is to the left of the line segment, we make the bend // If the point is to the left of the line segment, we make the bend
// negative, otherwise it's positive. // negative, otherwise it's positive.
const angleToBend = vec.angle(start.point, bend.point) const angleToBend = vec.angle(start.point, bendPoint)
if (isAngleBetween(angle, angle + Math.PI / 2, angleToBend)) { if (isAngleBetween(angle, angle + Math.PI, angleToBend)) {
shape.bend *= -1 shape.bend *= -1
} }
} }
@ -520,11 +525,13 @@ function getBendPoint(shape: ArrowShape) {
const bendDist = (dist / 2) * shape.bend const bendDist = (dist / 2) * shape.bend
const u = vec.uni(vec.vec(start.point, end.point)) const u = vec.uni(vec.vec(start.point, end.point))
return vec.round( const point = vec.round(
Math.abs(bendDist) < 10 Math.abs(bendDist) < 10
? midPoint ? midPoint
: vec.add(midPoint, vec.mul(vec.per(u), bendDist)) : vec.add(midPoint, vec.mul(vec.per(u), bendDist))
) )
return point
} }
function renderFreehandArrowShaft(shape: ArrowShape) { function renderFreehandArrowShaft(shape: ArrowShape) {
@ -561,37 +568,68 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
return path return path
} }
function getArrowHeadPath(shape: ArrowShape, point: number[], angle = 0) { function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
const { left, right } = getArrowHeadPoints(shape, point, angle) const { style, id } = shape
const { start, end } = shape.handles
const getRandom = rng(id)
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
const st = Math.abs(getRandom())
const center = [circle[0], circle[1]]
const radius = circle[2]
const startAngle = vec.angle(center, start.point)
const endAngle = vec.angle(center, end.point)
const points: number[][] = []
for (let i = 0; i < 14; i++) {
const t = i / 13
const angle = lerpAngles(startAngle, endAngle, t)
points.push(vec.round(vec.nudgeAtAngle(center, angle, radius)))
}
const stroke = getStroke(
[...points, end.point, end.point, end.point, end.point],
{
size: strokeWidth / 2,
thinning: 0.5 + getRandom() * 0.3,
easing: (t) => t * t,
end: { taper: 1 },
start: { taper: 1 + 32 * (st * st * st) },
simulatePressure: true,
last: true,
}
)
const path = getSvgPathFromStroke(stroke)
return path
}
function getArrowHeadPath(shape: ArrowShape, point: number[], inset: number[]) {
const { left, right } = getArrowHeadPoints(shape, point, inset)
return ['M', left, 'L', point, right].join(' ') return ['M', left, 'L', point, right].join(' ')
} }
function getArrowHeadPoints(shape: ArrowShape, point: number[], angle = 0) { function getArrowHeadPoints(
const { start, end } = shape.handles shape: ArrowShape,
point: number[],
const stroke = +getShapeStyle(shape.style).strokeWidth * 2 inset: number[]
) {
const arrowDist = vec.dist(start.point, end.point)
const arrowHeadlength = Math.min(arrowDist / 3, stroke * 4)
// Unit vector from start to end
const u = vec.uni(vec.vec(start.point, end.point))
// The end of the arrowhead wings
const v = vec.rot(vec.mul(vec.neg(u), arrowHeadlength), angle)
// Use the shape's random seed to create minor offsets for the angles // Use the shape's random seed to create minor offsets for the angles
const getRandom = rng(shape.id) const getRandom = rng(shape.id)
return { return {
left: vec.add( left: vec.rotWith(inset, point, Math.PI / 6 + (Math.PI / 12) * getRandom()),
right: vec.rotWith(
inset,
point, point,
vec.rot(v, Math.PI / 6 + (Math.PI / 12) * getRandom()) -Math.PI / 6 + (Math.PI / 12) * getRandom()
),
right: vec.add(
point,
vec.rot(v, -(Math.PI / 6) + (Math.PI / 12) * getRandom())
), ),
} }
} }

View file

@ -196,20 +196,6 @@ export function getClosestPointOnCircle(
return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r)) return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
} }
function det(
a: number,
b: number,
c: number,
d: number,
e: number,
f: number,
g: number,
h: number,
i: number
): number {
return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
}
/** /**
* Get a circle from three points. * Get a circle from three points.
* @param A * @param A
@ -222,47 +208,27 @@ export function circleFromThreePoints(
B: number[], B: number[],
C: number[] C: number[]
): number[] { ): number[] {
const a = det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1) const [x1, y1] = A
const [x2, y2] = B
const [x3, y3] = C
const bx = -det( const a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2
A[0] * A[0] + A[1] * A[1],
A[1],
1,
B[0] * B[0] + B[1] * B[1],
B[1],
1,
C[0] * C[0] + C[1] * C[1],
C[1],
1
)
const by = det(
A[0] * A[0] + A[1] * A[1],
A[0],
1,
B[0] * B[0] + B[1] * B[1],
B[0],
1,
C[0] * C[0] + C[1] * C[1],
C[0],
1
)
const c = -det(
A[0] * A[0] + A[1] * A[1],
A[0],
A[1],
B[0] * B[0] + B[1] * B[1],
B[0],
B[1],
C[0] * C[0] + C[1] * C[1],
C[0],
C[1]
)
const x = -bx / (2 * a) const b =
const y = -by / (2 * a) (x1 * x1 + y1 * y1) * (y3 - y2) +
const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a)) (x2 * x2 + y2 * y2) * (y1 - y3) +
(x3 * x3 + y3 * y3) * (y2 - y1)
return [x, y, r] const c =
(x1 * x1 + y1 * y1) * (x2 - x3) +
(x2 * x2 + y2 * y2) * (x3 - x1) +
(x3 * x3 + y3 * y3) * (x1 - x2)
const x = -b / (2 * a)
const y = -c / (2 * a)
return [x, y, Math.hypot(x - x1, y - y1)]
} }
/** /**
@ -281,7 +247,12 @@ export function perimeterOfEllipse(rx: number, ry: number): number {
* @param a0 * @param a0
* @param a1 * @param a1
*/ */
export function shortAngleDist(a0: number, a1: number): number { export function shortAngleDist(a0: number, a1: number, clamp = true): number {
if (!clamp) {
const da = a1 - a0
return 2 * da - da
}
const max = Math.PI * 2 const max = Math.PI * 2
const da = (a1 - a0) % max const da = (a1 - a0) % max
return ((2 * da) % max) - da return ((2 * da) % max) - da
@ -303,7 +274,7 @@ export function longAngleDist(a0: number, a1: number): number {
* @param t * @param t
*/ */
export function lerpAngles(a0: number, a1: number, t: number): number { export function lerpAngles(a0: number, a1: number, t: number): number {
return a0 + shortAngleDist(a0, a1) * t return a0 + shortAngleDist(a0, a1, true) * t
} }
/** /**