Improves arrowheads, curved arrows
This commit is contained in:
parent
a5c07c3efd
commit
64fa2a19b1
2 changed files with 128 additions and 119 deletions
|
@ -12,6 +12,8 @@ import {
|
|||
isAngleBetween,
|
||||
getPerfectDashProps,
|
||||
clampToRotationToSegments,
|
||||
lerpAngles,
|
||||
clamp,
|
||||
} from 'utils'
|
||||
import {
|
||||
ArrowShape,
|
||||
|
@ -106,26 +108,26 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
const isStraightLine =
|
||||
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 strokeWidth = +styles.strokeWidth
|
||||
|
||||
const sw = strokeWidth * 1.618
|
||||
|
||||
const arrowDist = vec.dist(start.point, end.point)
|
||||
|
||||
const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8)
|
||||
|
||||
let shaftPath: JSX.Element
|
||||
let startAngle: number
|
||||
let endAngle: number
|
||||
let insetStart: number[]
|
||||
let insetEnd: number[]
|
||||
|
||||
if (isStraightLine) {
|
||||
const straight_sw =
|
||||
strokeWidth * (style.dash === DashStyle.Draw ? 0.618 : 1.618)
|
||||
const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
|
||||
|
||||
const path =
|
||||
shape.style.dash === DashStyle.Draw
|
||||
? renderFreehandArrowShaft(shape)
|
||||
: 'M' + vec.round(start.point) + 'L' + vec.round(end.point)
|
||||
const path = isDraw
|
||||
? renderFreehandArrowShaft(shape)
|
||||
: 'M' + vec.round(start.point) + 'L' + vec.round(end.point)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
arrowDist,
|
||||
|
@ -134,9 +136,8 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
2
|
||||
)
|
||||
|
||||
startAngle = Math.PI
|
||||
|
||||
endAngle = 0
|
||||
insetStart = vec.nudge(start.point, end.point, arrowHeadlength)
|
||||
insetEnd = vec.nudge(end.point, start.point, arrowHeadlength)
|
||||
|
||||
// Straight arrow path
|
||||
shaftPath = (
|
||||
|
@ -154,7 +155,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
d={path}
|
||||
fill={styles.stroke}
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={straight_sw}
|
||||
strokeWidth={sw}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
|
@ -164,29 +165,34 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
} else {
|
||||
const circle = getCtp(shape)
|
||||
|
||||
const path = getArrowArcPath(start, end, circle, bend)
|
||||
const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
|
||||
|
||||
const path = isDraw
|
||||
? renderCurvedFreehandArrowShaft(shape, circle)
|
||||
: getArrowArcPath(start, end, circle, bend)
|
||||
|
||||
const arcLength = getArcLength(
|
||||
[circle[0], circle[1]],
|
||||
circle[2],
|
||||
start.point,
|
||||
end.point
|
||||
)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
getArcLength(
|
||||
[circle[0], circle[1]],
|
||||
circle[2],
|
||||
start.point,
|
||||
end.point
|
||||
) - 1,
|
||||
arcLength - 1,
|
||||
sw,
|
||||
shape.style.dash,
|
||||
2
|
||||
)
|
||||
|
||||
startAngle =
|
||||
vec.angle([circle[0], circle[1]], start.point) -
|
||||
vec.angle(end.point, start.point) +
|
||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
||||
const center = [circle[0], circle[1]]
|
||||
const radius = circle[2]
|
||||
const sa = vec.angle(center, start.point)
|
||||
const ea = vec.angle(center, end.point)
|
||||
const t = arrowHeadlength / Math.abs(arcLength)
|
||||
|
||||
endAngle =
|
||||
vec.angle([circle[0], circle[1]], end.point) -
|
||||
vec.angle(start.point, end.point) +
|
||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
||||
insetStart = vec.nudgeAtAngle(center, lerpAngles(sa, ea, t), radius)
|
||||
insetEnd = vec.nudgeAtAngle(center, lerpAngles(ea, sa, t), radius)
|
||||
|
||||
// Curved arrow path
|
||||
shaftPath = (
|
||||
|
@ -202,7 +208,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
/>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
fill={isDraw ? styles.stroke : 'none'}
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={sw}
|
||||
strokeDasharray={strokeDasharray}
|
||||
|
@ -218,20 +224,20 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
{shaftPath}
|
||||
{shape.decorations.start === Decoration.Arrow && (
|
||||
<path
|
||||
d={getArrowHeadPath(shape, start.point, startAngle)}
|
||||
d={getArrowHeadPath(shape, start.point, insetStart)}
|
||||
fill="none"
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={sw}
|
||||
strokeWidth={strokeWidth * 1.618}
|
||||
strokeDashoffset="none"
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
)}
|
||||
{shape.decorations.end === Decoration.Arrow && (
|
||||
<path
|
||||
d={getArrowHeadPath(shape, end.point, endAngle)}
|
||||
d={getArrowHeadPath(shape, end.point, insetEnd)}
|
||||
fill="none"
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={sw}
|
||||
strokeWidth={strokeWidth * 1.618}
|
||||
strokeDashoffset="none"
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
|
@ -434,23 +440,22 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
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 bendPoint = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
||||
|
||||
// Find the distance between the midpoint and the nearest point on the
|
||||
// line segment to the bend handle's dragged point
|
||||
const bendDist = vec.dist(
|
||||
midPoint,
|
||||
vec.round(vec.nearestPointOnLineSegment(ap, bp, bend.point, true))
|
||||
)
|
||||
const bendDist = vec.dist(midPoint, bendPoint)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
@ -520,11 +525,13 @@ function getBendPoint(shape: ArrowShape) {
|
|||
const bendDist = (dist / 2) * shape.bend
|
||||
const u = vec.uni(vec.vec(start.point, end.point))
|
||||
|
||||
return vec.round(
|
||||
const point = vec.round(
|
||||
Math.abs(bendDist) < 10
|
||||
? midPoint
|
||||
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||
)
|
||||
|
||||
return point
|
||||
}
|
||||
|
||||
function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||
|
@ -561,37 +568,68 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
|||
return path
|
||||
}
|
||||
|
||||
function getArrowHeadPath(shape: ArrowShape, point: number[], angle = 0) {
|
||||
const { left, right } = getArrowHeadPoints(shape, point, angle)
|
||||
function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
|
||||
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(' ')
|
||||
}
|
||||
|
||||
function getArrowHeadPoints(shape: ArrowShape, point: number[], angle = 0) {
|
||||
const { start, end } = shape.handles
|
||||
|
||||
const stroke = +getShapeStyle(shape.style).strokeWidth * 2
|
||||
|
||||
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)
|
||||
|
||||
function getArrowHeadPoints(
|
||||
shape: ArrowShape,
|
||||
point: number[],
|
||||
inset: number[]
|
||||
) {
|
||||
// Use the shape's random seed to create minor offsets for the angles
|
||||
const getRandom = rng(shape.id)
|
||||
|
||||
return {
|
||||
left: vec.add(
|
||||
left: vec.rotWith(inset, point, Math.PI / 6 + (Math.PI / 12) * getRandom()),
|
||||
right: vec.rotWith(
|
||||
inset,
|
||||
point,
|
||||
vec.rot(v, Math.PI / 6 + (Math.PI / 12) * getRandom())
|
||||
),
|
||||
right: vec.add(
|
||||
point,
|
||||
vec.rot(v, -(Math.PI / 6) + (Math.PI / 12) * getRandom())
|
||||
-Math.PI / 6 + (Math.PI / 12) * getRandom()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,20 +196,6 @@ export function getClosestPointOnCircle(
|
|||
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.
|
||||
* @param A
|
||||
|
@ -222,47 +208,27 @@ export function circleFromThreePoints(
|
|||
B: number[],
|
||||
C: 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(
|
||||
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 a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2
|
||||
|
||||
const x = -bx / (2 * a)
|
||||
const y = -by / (2 * a)
|
||||
const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
|
||||
const b =
|
||||
(x1 * x1 + y1 * y1) * (y3 - y2) +
|
||||
(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 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 da = (a1 - a0) % max
|
||||
return ((2 * da) % max) - da
|
||||
|
@ -303,7 +274,7 @@ export function longAngleDist(a0: number, a1: number): number {
|
|||
* @param t
|
||||
*/
|
||||
export function lerpAngles(a0: number, a1: number, t: number): number {
|
||||
return a0 + shortAngleDist(a0, a1) * t
|
||||
return a0 + shortAngleDist(a0, a1, true) * t
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue