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,
|
isAngleBetween,
|
||||||
getPerfectDashProps,
|
getPerfectDashProps,
|
||||||
clampToRotationToSegments,
|
clampToRotationToSegments,
|
||||||
|
lerpAngles,
|
||||||
|
clamp,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import {
|
import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
|
@ -106,26 +108,26 @@ 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)
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
arrowDist,
|
arrowDist,
|
||||||
|
@ -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 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(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
getArcLength(
|
arcLength - 1,
|
||||||
[circle[0], circle[1]],
|
|
||||||
circle[2],
|
|
||||||
start.point,
|
|
||||||
end.point
|
|
||||||
) - 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())
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue