init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions
@@ -0,0 +1,268 @@
import { Utils } from '@tldraw/core';
import {
intersectCircleCircle,
intersectCircleLineSegment,
} from '@tldraw/intersect';
import Vec from '@tldraw/vec';
import getStroke from 'perfect-freehand';
import { EASINGS } from '@toeverything/components/board-types';
import { getShapeStyle } from '../shared/shape-styles';
import type {
ArrowShape,
Decoration,
ShapeStyles,
} from '@toeverything/components/board-types';
export function getArrowArcPath(
start: number[],
end: number[],
circle: number[],
bend: number
) {
return [
'M',
start[0],
start[1],
'A',
circle[2],
circle[2],
0,
0,
bend < 0 ? 0 : 1,
end[0],
end[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(
id: string,
style: ShapeStyles,
start: number[],
end: number[],
decorationStart: Decoration | undefined,
decorationEnd: Decoration | undefined
) {
const getRandom = Utils.rng(id);
const strokeWidth = getShapeStyle(style).strokeWidth;
const startPoint = decorationStart
? Vec.nudge(start, end, strokeWidth)
: start;
const endPoint = decorationEnd ? Vec.nudge(end, start, strokeWidth) : end;
const stroke = getStroke([startPoint, endPoint], {
size: strokeWidth,
thinning: 0.618 + getRandom() * 0.2,
easing: EASINGS.easeOutQuad,
simulatePressure: true,
streamline: 0,
last: true,
});
return Utils.getSvgPathFromStroke(stroke);
}
export function renderCurvedFreehandArrowShaft(
id: string,
style: ShapeStyles,
start: number[],
end: number[],
decorationStart: Decoration | undefined,
decorationEnd: Decoration | undefined,
center: number[],
radius: number,
length: number,
easing: (t: number) => number
) {
const getRandom = Utils.rng(id);
const strokeWidth = getShapeStyle(style).strokeWidth;
const startPoint = decorationStart
? Vec.rotWith(start, center, strokeWidth / length)
: start;
const endPoint = decorationEnd
? Vec.rotWith(end, center, -(strokeWidth / length))
: end;
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,
});
return Utils.getSvgPathFromStroke(stroke);
}
export function getCtp(start: number[], bend: number[], end: number[]) {
return Utils.circleFromThreePoints(start, end, bend);
}
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(
style: ShapeStyles,
start: number[],
bend: number[],
end: number[],
decorationStart: Decoration | undefined,
decorationEnd: Decoration | undefined
) {
const { strokeWidth } = getShapeStyle(style, false);
const arrowDist = Vec.dist(start, end);
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8);
const path: (string | number)[] = [];
const isStraightLine = Vec.dist(bend, Vec.toFixed(Vec.med(start, end))) < 1;
if (isStraightLine) {
path.push(`M ${start} L ${end}`);
if (decorationStart) {
path.push(getStraightArrowHeadPath(start, end, arrowHeadLength));
}
if (decorationEnd) {
path.push(getStraightArrowHeadPath(end, start, arrowHeadLength));
}
} else {
const circle = getCtp(start, bend, end);
const center = [circle[0], circle[1]];
const radius = circle[2];
const length = getArcLength(center, radius, start, end);
path.push(
`M ${start} A ${radius} ${radius} 0 0 ${
length > 0 ? '1' : '0'
} ${end}`
);
if (decorationStart)
path.push(
getCurvedArrowHeadPath(
start,
arrowHeadLength,
center,
radius,
length < 0
)
);
if (decorationEnd) {
path.push(
getCurvedArrowHeadPath(
end,
arrowHeadLength,
center,
radius,
length >= 0
)
);
}
}
return path.join(' ');
}
export function getArcPoints(start: number[], bend: number[], end: number[]) {
if (Vec.dist2(bend, Vec.med(start, end)) <= 4) return [start, end];
// The arc is curved; calculate twenty points along the arc
const points: number[][] = [];
const circle = getCtp(start, bend, end);
const center = [circle[0], circle[1]];
const radius = circle[2];
const startAngle = Vec.angle(center, start);
const endAngle = Vec.angle(center, end);
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;
}
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));
}
@@ -0,0 +1,604 @@
import * as React from 'react';
import { Utils, TLBounds, SVGContainer } from '@tldraw/core';
import { Vec } from '@tldraw/vec';
import { defaultStyle } from '../shared/shape-styles';
import {
ArrowShape,
TransformInfo,
Decoration,
TDShapeType,
DashStyle,
TDMeta,
GHOSTED_OPACITY,
} from '@toeverything/components/board-types';
import { TDShapeUtil } from '../TDShapeUtil';
import {
intersectArcBounds,
intersectLineSegmentBounds,
intersectLineSegmentLineSegment,
} from '@tldraw/intersect';
import {
getArcLength,
getArcPoints,
getArrowPath,
getBendPoint,
getCtp,
isAngleBetween,
} from './arrow-helpers';
import { styled } from '@toeverything/components/ui';
import {
TextLabel,
getFontStyle,
getShapeStyle,
getTextLabelSize,
LabelMask,
} from '../shared';
import { StraightArrow } from './components/straight-arrow';
import { CurvedArrow } from './components/curved-arrow';
type T = ArrowShape;
type E = HTMLDivElement;
export class ArrowUtil extends TDShapeUtil<T, E> {
type = TDShapeType.Arrow as const;
override hideBounds = true;
override canEdit = true;
pathCache = new WeakMap<T, string>();
getShape = (props: Partial<T>): T => {
return {
id: 'id',
type: TDShapeType.Arrow,
name: 'Arrow',
parentId: 'page',
childIndex: 1,
point: [0, 0],
rotation: 0,
bend: 0,
handles: {
start: {
id: 'start',
index: 0,
point: [0, 0],
canBind: true,
...props.handles?.start,
},
end: {
id: 'end',
index: 1,
point: [1, 1],
canBind: true,
...props.handles?.end,
},
bend: {
id: 'bend',
index: 2,
point: [0.5, 0.5],
...props.handles?.bend,
},
},
decorations: props.decorations ?? {
end: Decoration.Arrow,
},
style: {
...defaultStyle,
isFilled: false,
...props.style,
},
label: '',
labelPoint: [0.5, 0.5],
workspace: props.workspace,
...props,
};
};
Component = TDShapeUtil.Component<T, E, TDMeta>(
(
{
shape,
isEditing,
isGhost,
meta,
events,
onShapeChange,
onShapeBlur,
},
ref
) => {
const {
id,
label = '',
handles: { start, bend, end },
decorations = {},
style,
} = shape;
const isStraightLine =
Vec.dist(
bend.point,
Vec.toFixed(Vec.med(start.point, end.point))
) < 1;
const font = getFontStyle(style);
const styles = getShapeStyle(style, meta.isDarkMode);
const labelSize =
label || isEditing ? getTextLabelSize(label, font) : [0, 0];
const bounds = this.getBounds(shape);
const dist = React.useMemo(() => {
const { start, bend, end } = shape.handles;
if (isStraightLine) return Vec.dist(start.point, end.point);
const circle = getCtp(start.point, bend.point, end.point);
const center = circle.slice(0, 2);
const radius = circle[2];
const length = getArcLength(
center,
radius,
start.point,
end.point
);
return Math.abs(length);
}, [shape.handles]);
const scale = Math.max(
0.5,
Math.min(
1,
Math.max(
dist / (labelSize[1] + 128),
dist / (labelSize[0] + 128)
)
)
);
const offset = React.useMemo(() => {
const bounds = this.getBounds(shape);
const offset = Vec.sub(
shape.handles.bend.point,
Vec.toFixed([bounds.width / 2, bounds.height / 2])
);
return offset;
}, [shape, scale]);
const handleLabelChange = React.useCallback(
(label: string) => {
onShapeChange?.({ id, label });
},
[onShapeChange]
);
const Component = isStraightLine ? StraightArrow : CurvedArrow;
return (
<FullWrapper ref={ref} {...events}>
<TextLabel
font={font}
text={label}
color={styles.stroke}
offsetX={offset[0]}
offsetY={offset[1]}
scale={scale}
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
/>
<SVGContainer id={shape.id + '_svg'}>
<defs>
<mask id={shape.id + '_clip'}>
<rect
x={-100}
y={-100}
width={bounds.width + 200}
height={bounds.height + 200}
fill="white"
/>
<rect
x={
bounds.width / 2 -
(labelSize[0] / 2) * scale +
offset[0]
}
y={
bounds.height / 2 -
(labelSize[1] / 2) * scale +
offset[1]
}
width={labelSize[0] * scale}
height={labelSize[1] * scale}
rx={4 * scale}
ry={4 * scale}
fill="black"
opacity={1}
/>
</mask>
</defs>
<g
pointerEvents="none"
opacity={isGhost ? GHOSTED_OPACITY : 1}
mask={
label || isEditing
? `url(#${shape.id}_clip)`
: ``
}
>
<Component
id={id}
style={style}
start={start.point}
end={end.point}
bend={bend.point}
arrowBend={shape.bend}
decorationStart={decorations?.start}
decorationEnd={decorations?.end}
isDraw={style.dash === DashStyle.Draw}
isDarkMode={meta.isDarkMode}
/>
</g>
</SVGContainer>
</FullWrapper>
);
}
);
Indicator = TDShapeUtil.Indicator<ArrowShape>(({ shape, bounds }) => {
const {
style,
decorations,
label,
handles: { start, bend, end },
} = shape;
const font = getFontStyle(style);
const labelSize = label ? getTextLabelSize(label, font) : [0, 0];
const isStraightLine =
Vec.dist(bend.point, Vec.toFixed(Vec.med(start.point, end.point))) <
1;
const dist = React.useMemo(() => {
const { start, bend, end } = shape.handles;
if (isStraightLine) return Vec.dist(start.point, end.point);
const circle = getCtp(start.point, bend.point, end.point);
const center = circle.slice(0, 2);
const radius = circle[2];
const length = getArcLength(center, radius, start.point, end.point);
return Math.abs(length);
}, [shape.handles]);
const scale = Math.max(
0.5,
Math.min(
1,
Math.max(
dist / (labelSize[1] + 128),
dist / (labelSize[0] + 128)
)
)
);
const offset = React.useMemo(() => {
const bounds = this.getBounds(shape);
const offset = Vec.sub(shape.handles.bend.point, [
bounds.width / 2,
bounds.height / 2,
]);
return offset;
}, [shape, scale]);
return (
<>
<LabelMask
id={shape.id}
scale={scale}
offset={offset}
bounds={bounds}
labelSize={labelSize}
/>
<path
d={getArrowPath(
style,
start.point,
bend.point,
end.point,
decorations?.start,
decorations?.end
)}
mask={label ? `url(#${shape.id}_clip)` : ``}
/>
{label && (
<rect
x={
bounds.width / 2 -
(labelSize[0] / 2) * scale +
offset[0]
}
y={
bounds.height / 2 -
(labelSize[1] / 2) * scale +
offset[1]
}
width={labelSize[0] * scale}
height={labelSize[1] * scale}
rx={4 * scale}
ry={4 * scale}
fill="transparent"
/>
)}
</>
);
});
getBounds = (shape: T) => {
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
const {
handles: { start, bend, end },
} = shape;
return Utils.getBoundsFromPoints(
getArcPoints(start.point, bend.point, end.point)
);
});
return Utils.translateBounds(bounds, shape.point);
};
override getRotatedBounds = (shape: T) => {
const {
handles: { start, bend, end },
} = shape;
let points = getArcPoints(start.point, bend.point, end.point);
const { minX, minY, maxX, maxY } = Utils.getBoundsFromPoints(points);
if (shape.rotation !== 0) {
points = points.map(pt =>
Vec.rotWith(
pt,
[(minX + maxX) / 2, (minY + maxY) / 2],
shape.rotation || 0
)
);
}
return Utils.translateBounds(
Utils.getBoundsFromPoints(points),
shape.point
);
};
override getCenter = (shape: T) => {
const { start, end } = shape.handles;
return Vec.add(shape.point, Vec.med(start.point, end.point));
};
override shouldRender = (prev: T, next: T) => {
return (
next.decorations !== prev.decorations ||
next.handles !== prev.handles ||
next.style !== prev.style ||
next.label !== prev.label
);
};
override hitTestPoint = (shape: T, point: number[]): boolean => {
const {
handles: { start, bend, end },
} = shape;
const pt = Vec.sub(point, shape.point);
const points = getArcPoints(start.point, bend.point, end.point);
for (let i = 1; i < points.length; i++) {
if (Vec.distanceToLineSegment(points[i - 1], points[i], pt) < 1) {
return true;
}
}
return false;
};
override hitTestLineSegment = (
shape: T,
A: number[],
B: number[]
): boolean => {
const {
handles: { start, bend, end },
} = shape;
const ptA = Vec.sub(A, shape.point);
const ptB = Vec.sub(B, shape.point);
const points = getArcPoints(start.point, bend.point, end.point);
for (let i = 1; i < points.length; i++) {
if (
intersectLineSegmentLineSegment(
points[i - 1],
points[i],
ptA,
ptB
).didIntersect
) {
return true;
}
}
return false;
};
override hitTestBounds = (shape: T, bounds: TLBounds) => {
const { start, end, bend } = shape.handles;
const sp = Vec.add(shape.point, start.point);
const ep = Vec.add(shape.point, end.point);
if (
Utils.pointInBounds(sp, bounds) ||
Utils.pointInBounds(ep, bounds)
) {
return true;
}
if (Vec.isEqual(Vec.med(start.point, end.point), bend.point)) {
return intersectLineSegmentBounds(sp, ep, bounds).length > 0;
} else {
const [cx, cy, r] = getCtp(start.point, bend.point, end.point);
const cp = Vec.add(shape.point, [cx, cy]);
return intersectArcBounds(cp, r, sp, ep, bounds).length > 0;
}
};
override transform = (
shape: T,
bounds: TLBounds,
{ initialShape, scaleX, scaleY }: TransformInfo<T>
): Partial<T> => {
const initialShapeBounds = this.getBounds(initialShape);
const handles: (keyof T['handles'])[] = ['start', 'end'];
const nextHandles = { ...initialShape.handles };
handles.forEach(handle => {
const [x, y] = nextHandles[handle].point;
const nw = x / initialShapeBounds.width;
const nh = y / initialShapeBounds.height;
nextHandles[handle] = {
...nextHandles[handle],
point: [
bounds.width * (scaleX < 0 ? 1 - nw : nw),
bounds.height * (scaleY < 0 ? 1 - nh : nh),
],
};
});
const { start, bend, end } = nextHandles;
const dist = Vec.dist(start.point, end.point);
const midPoint = Vec.med(start.point, end.point);
const bendDist = (dist / 2) * initialShape.bend;
const u = Vec.uni(Vec.vec(start.point, end.point));
const point = Vec.add(midPoint, Vec.mul(Vec.per(u), bendDist));
nextHandles['bend'] = {
...bend,
point: Vec.toFixed(Math.abs(bendDist) < 10 ? midPoint : point),
};
return {
point: Vec.toFixed([bounds.minX, bounds.minY]),
handles: nextHandles,
};
};
override onDoubleClickHandle = (
shape: T,
handle: Partial<T['handles']>
): Partial<T> | void => {
switch (handle) {
case 'bend': {
return {
bend: 0,
handles: {
...shape.handles,
bend: {
...shape.handles.bend,
point: getBendPoint(shape.handles, shape.bend),
},
},
};
}
case 'start': {
return {
decorations: {
...shape.decorations,
start: shape.decorations?.start
? undefined
: Decoration.Arrow,
},
};
}
case 'end': {
return {
decorations: {
...shape.decorations,
end: shape.decorations?.end
? undefined
: Decoration.Arrow,
},
};
}
}
return this;
};
override onHandleChange = (
shape: T,
handles: Partial<T['handles']>
): Partial<T> | void => {
let nextHandles = Utils.deepMerge<ArrowShape['handles']>(
shape.handles,
handles
);
let nextBend = shape.bend;
nextHandles = Utils.deepMerge(nextHandles, {
start: {
point: Vec.toFixed(nextHandles.start.point),
},
end: {
point: Vec.toFixed(nextHandles.end.point),
},
});
// This will produce NaN values
if (Vec.isEqual(nextHandles.start.point, nextHandles.end.point)) return;
// If the user is moving the bend handle, we want to move the bend point
if ('bend' in handles) {
const { start, end, bend } = nextHandles;
const distance = Vec.dist(start.point, end.point);
const midPoint = Vec.med(start.point, end.point);
const angle = Vec.angle(start.point, end.point);
const u = Vec.uni(Vec.vec(start.point, end.point));
// Create a line segment perendicular to the line between the start and end points
const ap = Vec.add(midPoint, Vec.mul(Vec.per(u), distance));
const bp = Vec.sub(midPoint, Vec.mul(Vec.per(u), distance));
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, 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.
const realBend = bendDist / (distance / 2);
nextBend = Utils.clamp(realBend, -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, bendPoint);
// If resulting bend is low enough that the handle will snap to center,
// then also snap the bend to center
if (Vec.isEqual(midPoint, getBendPoint(nextHandles, nextBend))) {
nextBend = 0;
} else if (isAngleBetween(angle, angle + Math.PI, angleToBend)) {
// Otherwise, fix the bend direction
nextBend *= -1;
}
}
const nextShape = {
point: shape.point,
bend: nextBend,
handles: {
...nextHandles,
bend: {
...nextHandles.bend,
point: getBendPoint(nextHandles, nextBend),
},
},
};
// Zero out the handles to prevent handles with negative points. If a handle's x or y
// is below zero, we need to move the shape left or up to make it zero.
const topLeft = shape.point;
const nextBounds = this.getBounds({ ...nextShape } as ArrowShape);
const offset = Vec.sub([nextBounds.minX, nextBounds.minY], topLeft);
if (!Vec.isEqual(offset, [0, 0])) {
Object.values(nextShape.handles).forEach(handle => {
handle.point = Vec.toFixed(Vec.sub(handle.point, offset));
});
nextShape.point = Vec.toFixed(Vec.add(nextShape.point, offset));
}
return nextShape;
};
}
const FullWrapper = styled('div')({ width: '100%', height: '100%' });
@@ -0,0 +1,35 @@
import * as React from 'react';
export interface ArrowheadProps {
left: number[];
middle: number[];
right: number[];
stroke: string;
strokeWidth: number;
}
export function Arrowhead({
left,
middle,
right,
stroke,
strokeWidth,
}: ArrowheadProps) {
return (
<g>
<path
className="tl-stroke-hitarea"
d={`M ${left} L ${middle} ${right}`}
/>
<path
d={`M ${left} L ${middle} ${right}`}
fill="none"
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
pointerEvents="none"
/>
</g>
);
}
@@ -0,0 +1,132 @@
import { Utils } from '@tldraw/core';
import Vec from '@tldraw/vec';
import * as React from 'react';
import { EASINGS } from '@toeverything/components/board-types';
import { getShapeStyle } from '../../shared';
import type {
Decoration,
ShapeStyles,
} from '@toeverything/components/board-types';
import {
getArcLength,
getArrowArcPath,
getCtp,
getCurvedArrowHeadPoints,
renderCurvedFreehandArrowShaft,
} from '../arrow-helpers';
import { Arrowhead } from './arrow-head';
interface ArrowSvgProps {
id: string;
style: ShapeStyles;
start: number[];
bend: number[];
end: number[];
arrowBend: number;
decorationStart: Decoration | undefined;
decorationEnd: Decoration | undefined;
isDarkMode: boolean;
isDraw: boolean;
}
export const CurvedArrow = React.memo(function CurvedArrow({
id,
style,
start,
bend,
end,
arrowBend,
decorationStart,
decorationEnd,
isDraw,
isDarkMode,
}: ArrowSvgProps) {
const arrowDist = Vec.dist(start, end);
if (arrowDist < 2) return null;
const styles = getShapeStyle(style, isDarkMode);
const { strokeWidth } = styles;
const sw = 1 + strokeWidth * 1.618;
// Calculate a path as a segment of a circle passing through the three points start, bend, and end
const circle = getCtp(start, bend, end);
const center = [circle[0], circle[1]];
const radius = circle[2];
const length = getArcLength(center, radius, start, end);
const getRandom = Utils.rng(id);
const easing =
EASINGS[getRandom() > 0 ? 'easeInOutSine' : 'easeInOutCubic'];
const path = isDraw
? renderCurvedFreehandArrowShaft(
id,
style,
start,
end,
decorationStart,
decorationEnd,
center,
radius,
length,
easing
)
: getArrowArcPath(start, end, circle, arrowBend);
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
Math.abs(length),
sw,
style.dash,
2,
false
);
// Arrowheads
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8);
const startArrowHead = decorationStart
? getCurvedArrowHeadPoints(
start,
arrowHeadLength,
center,
radius,
length < 0
)
: null;
const endArrowHead = decorationEnd
? getCurvedArrowHeadPoints(
end,
arrowHeadLength,
center,
radius,
length >= 0
)
: null;
return (
<>
<path className="tl-stroke-hitarea" d={path} />
<path
d={path}
fill={isDraw ? styles.stroke : 'none'}
stroke={styles.stroke}
strokeWidth={isDraw ? 0 : sw}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
strokeLinejoin="round"
pointerEvents="none"
/>
{startArrowHead && (
<Arrowhead
left={startArrowHead.left}
middle={start}
right={startArrowHead.right}
stroke={styles.stroke}
strokeWidth={sw}
/>
)}
{endArrowHead && (
<Arrowhead
left={endArrowHead.left}
middle={end}
right={endArrowHead.right}
stroke={styles.stroke}
strokeWidth={sw}
/>
)}
</>
);
});
@@ -0,0 +1,103 @@
import { Utils } from '@tldraw/core';
import Vec from '@tldraw/vec';
import * as React from 'react';
import { getShapeStyle } from '../../shared';
import type {
Decoration,
ShapeStyles,
} from '@toeverything/components/board-types';
import {
getStraightArrowHeadPoints,
renderFreehandArrowShaft,
} from '../arrow-helpers';
import { Arrowhead } from './arrow-head';
interface ArrowSvgProps {
id: string;
style: ShapeStyles;
start: number[];
bend: number[];
end: number[];
arrowBend: number;
decorationStart: Decoration | undefined;
decorationEnd: Decoration | undefined;
isDarkMode: boolean;
isDraw: boolean;
}
export const StraightArrow = React.memo(function StraightArrow({
id,
style,
start,
end,
decorationStart,
decorationEnd,
isDraw,
isDarkMode,
}: ArrowSvgProps) {
const arrowDist = Vec.dist(start, end);
if (arrowDist < 2) return null;
const styles = getShapeStyle(style, isDarkMode);
const { strokeWidth } = styles;
const sw = 1 + strokeWidth * 1.618;
// Path between start and end points
const path = isDraw
? renderFreehandArrowShaft(
id,
style,
start,
end,
decorationStart,
decorationEnd
)
: 'M' + Vec.toFixed(start) + 'L' + Vec.toFixed(end);
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
arrowDist,
strokeWidth * 1.618,
style.dash,
2,
false
);
// Arrowheads
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8);
const startArrowHead = decorationStart
? getStraightArrowHeadPoints(start, end, arrowHeadLength)
: null;
const endArrowHead = decorationEnd
? getStraightArrowHeadPoints(end, start, arrowHeadLength)
: null;
return (
<>
<path className="tl-stroke-hitarea" d={path} />
<path
d={path}
fill={styles.stroke}
stroke={styles.stroke}
strokeWidth={isDraw ? sw / 2 : sw}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
strokeLinejoin="round"
pointerEvents="stroke"
/>
{startArrowHead && (
<Arrowhead
left={startArrowHead.left}
middle={start}
right={startArrowHead.right}
stroke={styles.stroke}
strokeWidth={sw}
/>
)}
{endArrowHead && (
<Arrowhead
left={endArrowHead.left}
middle={end}
right={endArrowHead.right}
stroke={styles.stroke}
strokeWidth={sw}
/>
)}
</>
);
});
@@ -0,0 +1 @@
export * from './arrow-util';