您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

arrow.tsx 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. import vec from 'utils/vec'
  2. import {
  3. getArcLength,
  4. uniqueId,
  5. getSvgPathFromStroke,
  6. rng,
  7. getBoundsFromPoints,
  8. translateBounds,
  9. pointInBounds,
  10. circleFromThreePoints,
  11. isAngleBetween,
  12. getPerfectDashProps,
  13. clampToRotationToSegments,
  14. lerpAngles,
  15. clamp,
  16. getFromCache,
  17. } from 'utils'
  18. import {
  19. ArrowShape,
  20. DashStyle,
  21. Decoration,
  22. ShapeHandle,
  23. ShapeType,
  24. } from 'types'
  25. import {
  26. intersectArcBounds,
  27. intersectLineSegmentBounds,
  28. } from 'utils/intersections'
  29. import { defaultStyle, getShapeStyle } from 'state/shape-styles'
  30. import getStroke from 'perfect-freehand'
  31. import React from 'react'
  32. import { registerShapeUtils } from './register'
  33. // A cache for semi-expensive circles calculated from three points
  34. function getCtp(shape: ArrowShape) {
  35. const { start, end, bend } = shape.handles
  36. return circleFromThreePoints(start.point, end.point, bend.point)
  37. }
  38. const arrow = registerShapeUtils<ArrowShape>({
  39. boundsCache: new WeakMap([]),
  40. defaultProps: {
  41. id: uniqueId(),
  42. type: ShapeType.Arrow,
  43. name: 'Arrow',
  44. parentId: 'page1',
  45. childIndex: 0,
  46. point: [0, 0],
  47. rotation: 0,
  48. bend: 0,
  49. handles: {
  50. start: {
  51. id: 'start',
  52. index: 0,
  53. point: [0, 0],
  54. },
  55. end: {
  56. id: 'end',
  57. index: 1,
  58. point: [1, 1],
  59. },
  60. bend: {
  61. id: 'bend',
  62. index: 2,
  63. point: [0.5, 0.5],
  64. },
  65. },
  66. decorations: {
  67. start: null,
  68. middle: null,
  69. end: Decoration.Arrow,
  70. },
  71. style: {
  72. ...defaultStyle,
  73. isFilled: false,
  74. },
  75. },
  76. create(props) {
  77. const shape = {
  78. ...this.defaultProps,
  79. ...props,
  80. decorations: {
  81. ...this.defaultProps.decorations,
  82. ...props.decorations,
  83. },
  84. style: {
  85. ...this.defaultProps.style,
  86. ...props.style,
  87. isFilled: false,
  88. },
  89. }
  90. return shape
  91. },
  92. shouldRender(shape, prev) {
  93. return shape.handles !== prev.handles || shape.style !== prev.style
  94. },
  95. render(shape, { isDarkMode }) {
  96. const { bend, handles, style } = shape
  97. const { start, end, bend: _bend } = handles
  98. const isStraightLine =
  99. vec.dist(_bend.point, vec.round(vec.med(start.point, end.point))) < 1
  100. const isDraw = shape.style.dash === DashStyle.Draw
  101. const styles = getShapeStyle(style, isDarkMode)
  102. const { strokeWidth } = styles
  103. const arrowDist = vec.dist(start.point, end.point)
  104. const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8)
  105. let shaftPath: JSX.Element
  106. let insetStart: number[]
  107. let insetEnd: number[]
  108. if (isStraightLine) {
  109. const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
  110. const path = isDraw
  111. ? renderFreehandArrowShaft(shape)
  112. : 'M' + vec.round(start.point) + 'L' + vec.round(end.point)
  113. const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
  114. arrowDist,
  115. sw,
  116. shape.style.dash,
  117. 2
  118. )
  119. insetStart = vec.nudge(start.point, end.point, arrowHeadlength)
  120. insetEnd = vec.nudge(end.point, start.point, arrowHeadlength)
  121. // Straight arrow path
  122. shaftPath = (
  123. <>
  124. <path
  125. d={path}
  126. fill="none"
  127. strokeWidth={Math.max(8, strokeWidth * 2)}
  128. strokeDasharray="none"
  129. strokeDashoffset="none"
  130. strokeLinecap="round"
  131. strokeLinejoin="round"
  132. />
  133. <path
  134. d={path}
  135. fill={styles.stroke}
  136. stroke={styles.stroke}
  137. strokeWidth={sw}
  138. strokeDasharray={strokeDasharray}
  139. strokeDashoffset={strokeDashoffset}
  140. strokeLinecap="round"
  141. strokeLinejoin="round"
  142. />
  143. </>
  144. )
  145. } else {
  146. const circle = getCtp(shape)
  147. const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
  148. const path = isDraw
  149. ? renderCurvedFreehandArrowShaft(shape, circle)
  150. : getArrowArcPath(start, end, circle, bend)
  151. const arcLength = getArcLength(
  152. [circle[0], circle[1]],
  153. circle[2],
  154. start.point,
  155. end.point
  156. )
  157. const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
  158. arcLength - 1,
  159. sw,
  160. shape.style.dash,
  161. 2
  162. )
  163. const center = [circle[0], circle[1]]
  164. const radius = circle[2]
  165. const sa = vec.angle(center, start.point)
  166. const ea = vec.angle(center, end.point)
  167. const t = arrowHeadlength / Math.abs(arcLength)
  168. insetStart = vec.nudgeAtAngle(center, lerpAngles(sa, ea, t), radius)
  169. insetEnd = vec.nudgeAtAngle(center, lerpAngles(ea, sa, t), radius)
  170. // Curved arrow path
  171. shaftPath = (
  172. <>
  173. <path
  174. d={path}
  175. fill="none"
  176. stroke="transparent"
  177. strokeWidth={Math.max(8, strokeWidth * 2)}
  178. strokeDasharray="none"
  179. strokeDashoffset="none"
  180. strokeLinecap="round"
  181. strokeLinejoin="round"
  182. />
  183. <path
  184. d={path}
  185. fill={isDraw ? styles.stroke : 'none'}
  186. stroke={styles.stroke}
  187. strokeWidth={sw}
  188. strokeDasharray={strokeDasharray}
  189. strokeDashoffset={strokeDashoffset}
  190. strokeLinecap="round"
  191. strokeLinejoin="round"
  192. />
  193. </>
  194. )
  195. }
  196. const sw = strokeWidth * 1.618
  197. return (
  198. <g pointerEvents="all">
  199. {shaftPath}
  200. {shape.decorations.start === Decoration.Arrow && (
  201. <path
  202. d={getArrowHeadPath(shape, start.point, insetStart)}
  203. fill="none"
  204. stroke={styles.stroke}
  205. strokeWidth={sw}
  206. strokeDashoffset="none"
  207. strokeDasharray="none"
  208. strokeLinecap="round"
  209. strokeLinejoin="round"
  210. pointerEvents="stroke"
  211. />
  212. )}
  213. {shape.decorations.end === Decoration.Arrow && (
  214. <path
  215. d={getArrowHeadPath(shape, end.point, insetEnd)}
  216. fill="none"
  217. stroke={styles.stroke}
  218. strokeWidth={sw}
  219. strokeDashoffset="none"
  220. strokeDasharray="none"
  221. strokeLinecap="round"
  222. strokeLinejoin="round"
  223. pointerEvents="stroke"
  224. />
  225. )}
  226. </g>
  227. )
  228. },
  229. rotateBy(shape, delta) {
  230. const { start, end, bend } = shape.handles
  231. const mp = vec.med(start.point, end.point)
  232. start.point = vec.rotWith(start.point, mp, delta)
  233. end.point = vec.rotWith(end.point, mp, delta)
  234. bend.point = vec.rotWith(bend.point, mp, delta)
  235. this.onHandleChange(shape, shape.handles, {
  236. delta: [0, 0],
  237. shiftKey: false,
  238. })
  239. return this
  240. },
  241. rotateTo(shape, rotation, delta) {
  242. const { start, end, bend } = shape.handles
  243. const mp = vec.med(start.point, end.point)
  244. start.point = vec.rotWith(start.point, mp, delta)
  245. end.point = vec.rotWith(end.point, mp, delta)
  246. bend.point = vec.rotWith(bend.point, mp, delta)
  247. this.onHandleChange(shape, shape.handles, {
  248. delta: [0, 0],
  249. shiftKey: false,
  250. })
  251. return this
  252. },
  253. getBounds(shape) {
  254. const bounds = getFromCache(this.boundsCache, shape, (cache) => {
  255. const { start, bend, end } = shape.handles
  256. cache.set(
  257. shape,
  258. getBoundsFromPoints([start.point, bend.point, end.point])
  259. )
  260. })
  261. return translateBounds(bounds, shape.point)
  262. },
  263. getRotatedBounds(shape) {
  264. const { start, bend, end } = shape.handles
  265. return translateBounds(
  266. getBoundsFromPoints([start.point, bend.point, end.point], shape.rotation),
  267. shape.point
  268. )
  269. },
  270. getCenter(shape) {
  271. const { start, end } = shape.handles
  272. return vec.add(shape.point, vec.med(start.point, end.point))
  273. },
  274. hitTest() {
  275. return true
  276. },
  277. hitTestBounds(this, shape, brushBounds) {
  278. const { start, end, bend } = shape.handles
  279. const sp = vec.add(shape.point, start.point)
  280. const ep = vec.add(shape.point, end.point)
  281. if (pointInBounds(sp, brushBounds) || pointInBounds(ep, brushBounds)) {
  282. return true
  283. }
  284. if (vec.isEqual(vec.med(start.point, end.point), bend.point)) {
  285. return intersectLineSegmentBounds(sp, ep, brushBounds).length > 0
  286. } else {
  287. const [cx, cy, r] = getCtp(shape)
  288. const cp = vec.add(shape.point, [cx, cy])
  289. return intersectArcBounds(sp, ep, cp, r, brushBounds).length > 0
  290. }
  291. },
  292. transform(shape, bounds, { initialShape, scaleX, scaleY }) {
  293. const initialShapeBounds = this.getBounds(initialShape)
  294. // let nw = initialShape.point[0] / initialShapeBounds.width
  295. // let nh = initialShape.point[1] / initialShapeBounds.height
  296. // shape.point = [
  297. // bounds.width * (scaleX < 0 ? 1 - nw : nw),
  298. // bounds.height * (scaleY < 0 ? 1 - nh : nh),
  299. // ]
  300. shape.point = [bounds.minX, bounds.minY]
  301. const handles = ['start', 'end']
  302. handles.forEach((handle) => {
  303. const [x, y] = initialShape.handles[handle].point
  304. const nw = x / initialShapeBounds.width
  305. const nh = y / initialShapeBounds.height
  306. shape.handles[handle].point = [
  307. bounds.width * (scaleX < 0 ? 1 - nw : nw),
  308. bounds.height * (scaleY < 0 ? 1 - nh : nh),
  309. ]
  310. })
  311. const { start, bend, end } = shape.handles
  312. const dist = vec.dist(start.point, end.point)
  313. const midPoint = vec.med(start.point, end.point)
  314. const bendDist = (dist / 2) * initialShape.bend
  315. const u = vec.uni(vec.vec(start.point, end.point))
  316. const point = vec.add(midPoint, vec.mul(vec.per(u), bendDist))
  317. bend.point = Math.abs(bendDist) < 10 ? midPoint : point
  318. return this
  319. },
  320. onDoublePointHandle(shape, handle) {
  321. switch (handle) {
  322. case 'bend': {
  323. shape.bend = 0
  324. shape.handles.bend.point = getBendPoint(shape)
  325. break
  326. }
  327. case 'start': {
  328. shape.decorations.start = shape.decorations.start
  329. ? null
  330. : Decoration.Arrow
  331. break
  332. }
  333. case 'end': {
  334. shape.decorations.end = shape.decorations.end ? null : Decoration.Arrow
  335. break
  336. }
  337. }
  338. return this
  339. },
  340. onHandleChange(shape, handles, { shiftKey }) {
  341. // Apple changes to the handles
  342. for (const id in handles) {
  343. const handle = handles[id]
  344. shape.handles[handle.id] = handle
  345. }
  346. // If the user is holding shift, we want to snap the handles to angles
  347. for (const id in handles) {
  348. if ((id === 'start' || id === 'end') && shiftKey) {
  349. const point = handles[id].point
  350. const other = id === 'start' ? shape.handles.end : shape.handles.start
  351. const angle = vec.angle(other.point, point)
  352. const distance = vec.dist(other.point, point)
  353. const newAngle = clampToRotationToSegments(angle, 24)
  354. shape.handles[id].point = vec.nudgeAtAngle(
  355. other.point,
  356. newAngle,
  357. distance
  358. )
  359. }
  360. }
  361. // If the user is moving the bend handle, we want to move the bend point
  362. if ('bend' in handles) {
  363. const { start, end, bend } = shape.handles
  364. const distance = vec.dist(start.point, end.point)
  365. const midPoint = vec.med(start.point, end.point)
  366. const angle = vec.angle(start.point, end.point)
  367. const u = vec.uni(vec.vec(start.point, end.point))
  368. // Create a line segment perendicular to the line between the start and end points
  369. const ap = vec.add(midPoint, vec.mul(vec.per(u), distance / 2))
  370. const bp = vec.sub(midPoint, vec.mul(vec.per(u), distance / 2))
  371. const bendPoint = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
  372. // Find the distance between the midpoint and the nearest point on the
  373. // line segment to the bend handle's dragged point
  374. const bendDist = vec.dist(midPoint, bendPoint)
  375. // The shape's "bend" is the ratio of the bend to the distance between
  376. // the start and end points. If the bend is below a certain amount, the
  377. // bend should be zero.
  378. shape.bend = clamp(bendDist / (distance / 2), -0.99, 0.99)
  379. // If the point is to the left of the line segment, we make the bend
  380. // negative, otherwise it's positive.
  381. const angleToBend = vec.angle(start.point, bendPoint)
  382. if (isAngleBetween(angle, angle + Math.PI, angleToBend)) {
  383. shape.bend *= -1
  384. }
  385. }
  386. shape.handles.start.point = vec.round(shape.handles.start.point)
  387. shape.handles.end.point = vec.round(shape.handles.end.point)
  388. shape.handles.bend.point = getBendPoint(shape)
  389. return this
  390. },
  391. onSessionComplete(shape) {
  392. const bounds = this.getBounds(shape)
  393. const offset = vec.sub([bounds.minX, bounds.minY], shape.point)
  394. this.translateTo(shape, vec.add(shape.point, offset))
  395. const { start, end, bend } = shape.handles
  396. start.point = vec.round(vec.sub(start.point, offset))
  397. end.point = vec.round(vec.sub(end.point, offset))
  398. bend.point = vec.round(vec.sub(bend.point, offset))
  399. shape.handles = { ...shape.handles }
  400. return this
  401. },
  402. applyStyles(shape, style) {
  403. Object.assign(shape.style, style)
  404. shape.style.isFilled = false
  405. return this
  406. },
  407. canStyleFill: false,
  408. })
  409. export default arrow
  410. function getArrowArcPath(
  411. start: ShapeHandle,
  412. end: ShapeHandle,
  413. circle: number[],
  414. bend: number
  415. ) {
  416. return [
  417. 'M',
  418. start.point[0],
  419. start.point[1],
  420. 'A',
  421. circle[2],
  422. circle[2],
  423. 0,
  424. 0,
  425. bend < 0 ? 0 : 1,
  426. end.point[0],
  427. end.point[1],
  428. ].join(' ')
  429. }
  430. function getBendPoint(shape: ArrowShape) {
  431. const { start, end } = shape.handles
  432. const dist = vec.dist(start.point, end.point)
  433. const midPoint = vec.med(start.point, end.point)
  434. const bendDist = (dist / 2) * shape.bend
  435. const u = vec.uni(vec.vec(start.point, end.point))
  436. const point = vec.round(
  437. Math.abs(bendDist) < 10
  438. ? midPoint
  439. : vec.add(midPoint, vec.mul(vec.per(u), bendDist))
  440. )
  441. return point
  442. }
  443. function renderFreehandArrowShaft(shape: ArrowShape) {
  444. const { style, id } = shape
  445. const { start, end } = shape.handles
  446. const getRandom = rng(id)
  447. const strokeWidth = +getShapeStyle(style).strokeWidth * 2
  448. const st = Math.abs(getRandom())
  449. const stroke = getStroke(
  450. [
  451. ...vec.pointsBetween(start.point, end.point),
  452. end.point,
  453. end.point,
  454. end.point,
  455. end.point,
  456. ],
  457. {
  458. size: strokeWidth / 2,
  459. thinning: 0.5 + getRandom() * 0.3,
  460. easing: (t) => t * t,
  461. end: { taper: 1 },
  462. start: { taper: 1 + 32 * (st * st * st) },
  463. simulatePressure: true,
  464. last: true,
  465. }
  466. )
  467. const path = getSvgPathFromStroke(stroke)
  468. return path
  469. }
  470. function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
  471. const { style, id } = shape
  472. const { start, end } = shape.handles
  473. const getRandom = rng(id)
  474. const strokeWidth = +getShapeStyle(style).strokeWidth * 2
  475. const st = Math.abs(getRandom())
  476. const center = [circle[0], circle[1]]
  477. const radius = circle[2]
  478. const startAngle = vec.angle(center, start.point)
  479. const endAngle = vec.angle(center, end.point)
  480. const points: number[][] = []
  481. for (let i = 0; i < 21; i++) {
  482. const t = i / 20
  483. const angle = lerpAngles(startAngle, endAngle, t)
  484. points.push(vec.round(vec.nudgeAtAngle(center, angle, radius)))
  485. }
  486. const stroke = getStroke([...points, end.point, end.point], {
  487. size: strokeWidth / 2,
  488. thinning: 0.5 + getRandom() * 0.3,
  489. easing: (t) => t * t,
  490. end: {
  491. taper: shape.decorations.end ? 1 : 1 + strokeWidth * 5 * (st * st * st),
  492. },
  493. start: {
  494. taper: shape.decorations.start ? 1 : 1 + strokeWidth * 5 * (st * st * st),
  495. },
  496. simulatePressure: true,
  497. streamline: 0.01,
  498. last: true,
  499. })
  500. const path = getSvgPathFromStroke(stroke)
  501. return path
  502. }
  503. function getArrowHeadPath(shape: ArrowShape, point: number[], inset: number[]) {
  504. const { left, right } = getArrowHeadPoints(shape, point, inset)
  505. return ['M', left, 'L', point, right].join(' ')
  506. }
  507. function getArrowHeadPoints(
  508. shape: ArrowShape,
  509. point: number[],
  510. inset: number[]
  511. ) {
  512. // Use the shape's random seed to create minor offsets for the angles
  513. const getRandom = rng(shape.id)
  514. return {
  515. left: vec.rotWith(inset, point, Math.PI / 6 + (Math.PI / 12) * getRandom()),
  516. right: vec.rotWith(
  517. inset,
  518. point,
  519. -Math.PI / 6 + (Math.PI / 12) * getRandom()
  520. ),
  521. }
  522. }