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

draw.tsx 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import { getFromCache, uniqueId } from 'utils/utils'
  2. import vec from 'utils/vec'
  3. import { DashStyle, DrawShape, ShapeType } from 'types'
  4. import { intersectPolylineBounds } from 'utils/intersections'
  5. import getStroke, { getStrokePoints } from 'perfect-freehand'
  6. import {
  7. getBoundsCenter,
  8. getBoundsFromPoints,
  9. getSvgPathFromStroke,
  10. translateBounds,
  11. boundsContain,
  12. } from 'utils'
  13. import { defaultStyle, getShapeStyle } from 'state/shape-styles'
  14. import { registerShapeUtils } from './register'
  15. const rotatedCache = new WeakMap<DrawShape, number[][]>([])
  16. const drawPathCache = new WeakMap<DrawShape['points'], string>([])
  17. const simplePathCache = new WeakMap<DrawShape['points'], string>([])
  18. const polygonCache = new WeakMap<DrawShape['points'], string>([])
  19. const draw = registerShapeUtils<DrawShape>({
  20. boundsCache: new WeakMap([]),
  21. canStyleFill: true,
  22. defaultProps: {
  23. id: uniqueId(),
  24. type: ShapeType.Draw,
  25. name: 'Draw',
  26. parentId: 'page1',
  27. childIndex: 0,
  28. point: [0, 0],
  29. points: [],
  30. rotation: 0,
  31. style: defaultStyle,
  32. },
  33. shouldRender(shape, prev) {
  34. return shape.points !== prev.points || shape.style !== prev.style
  35. },
  36. render(shape, { isHovered, isDarkMode }) {
  37. const { points, style } = shape
  38. const styles = getShapeStyle(style, isDarkMode)
  39. const strokeWidth = +styles.strokeWidth
  40. const shouldFill =
  41. style.isFilled &&
  42. points.length > 3 &&
  43. vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2
  44. // For very short lines, draw a point instead of a line
  45. if (points.length > 0 && points.length < 3) {
  46. const sw = strokeWidth * 0.618
  47. return (
  48. <circle
  49. r={strokeWidth * 0.618}
  50. fill={styles.stroke}
  51. stroke={styles.stroke}
  52. strokeWidth={sw}
  53. pointerEvents="all"
  54. filter={isHovered ? 'url(#expand)' : 'none'}
  55. />
  56. )
  57. }
  58. // For drawn lines, draw a line from the path cache
  59. if (shape.style.dash === DashStyle.Draw) {
  60. const polygonPathData = getFromCache(polygonCache, points, (cache) => {
  61. cache.set(shape.points, getFillPath(shape))
  62. })
  63. const drawPathData = getFromCache(drawPathCache, points, (cache) => {
  64. cache.set(shape.points, getDrawStrokePath(shape))
  65. })
  66. return (
  67. <>
  68. {shouldFill && (
  69. <path
  70. d={polygonPathData}
  71. stroke="none"
  72. fill={styles.fill}
  73. strokeLinejoin="round"
  74. strokeLinecap="round"
  75. pointerEvents="fill"
  76. />
  77. )}
  78. <path
  79. d={drawPathData}
  80. fill={styles.stroke}
  81. stroke={styles.stroke}
  82. strokeWidth={strokeWidth}
  83. strokeLinejoin="round"
  84. strokeLinecap="round"
  85. pointerEvents="all"
  86. filter={isHovered ? 'url(#expand)' : 'none'}
  87. />
  88. </>
  89. )
  90. }
  91. // For solid, dash and dotted lines, draw a regular stroke path
  92. const strokeDasharray = {
  93. [DashStyle.Draw]: 'none',
  94. [DashStyle.Solid]: `none`,
  95. [DashStyle.Dotted]: `${strokeWidth / 10} ${strokeWidth * 3}`,
  96. [DashStyle.Dashed]: `${strokeWidth * 3} ${strokeWidth * 3}`,
  97. }[style.dash]
  98. const strokeDashoffset = {
  99. [DashStyle.Draw]: 'none',
  100. [DashStyle.Solid]: `none`,
  101. [DashStyle.Dotted]: `-${strokeWidth / 20}`,
  102. [DashStyle.Dashed]: `-${strokeWidth}`,
  103. }[style.dash]
  104. if (!simplePathCache.has(points)) {
  105. simplePathCache.set(points, getSolidStrokePath(shape))
  106. }
  107. const path = simplePathCache.get(points)
  108. const sw = strokeWidth * 1.618
  109. return (
  110. <>
  111. <path
  112. d={path}
  113. fill={shouldFill ? styles.fill : 'none'}
  114. stroke="transparent"
  115. strokeWidth={Math.min(4, strokeWidth * 2)}
  116. strokeLinejoin="round"
  117. strokeLinecap="round"
  118. pointerEvents={shouldFill ? 'all' : 'stroke'}
  119. />
  120. <path
  121. d={path}
  122. fill="transparent"
  123. stroke={styles.stroke}
  124. strokeWidth={sw}
  125. strokeDasharray={strokeDasharray}
  126. strokeDashoffset={strokeDashoffset}
  127. strokeLinejoin="round"
  128. strokeLinecap="round"
  129. pointerEvents="stroke"
  130. filter={isHovered ? 'url(#expand)' : 'none'}
  131. />
  132. </>
  133. )
  134. },
  135. getBounds(shape) {
  136. const bounds = getFromCache(this.boundsCache, shape, (cache) => {
  137. cache.set(shape, getBoundsFromPoints(shape.points))
  138. })
  139. return translateBounds(bounds, shape.point)
  140. },
  141. getRotatedBounds(shape) {
  142. return translateBounds(
  143. getBoundsFromPoints(shape.points, shape.rotation),
  144. shape.point
  145. )
  146. },
  147. getCenter(shape) {
  148. return getBoundsCenter(this.getBounds(shape))
  149. },
  150. hitTest() {
  151. return true
  152. },
  153. hitTestBounds(this, shape, brushBounds) {
  154. // Test axis-aligned shape
  155. if (shape.rotation === 0) {
  156. return (
  157. boundsContain(brushBounds, this.getBounds(shape)) ||
  158. intersectPolylineBounds(
  159. shape.points,
  160. translateBounds(brushBounds, vec.neg(shape.point))
  161. ).length > 0
  162. )
  163. }
  164. // Test rotated shape
  165. const rBounds = this.getRotatedBounds(shape)
  166. const rotatedBounds = getFromCache(rotatedCache, shape, (cache) => {
  167. const c = getBoundsCenter(getBoundsFromPoints(shape.points))
  168. cache.set(
  169. shape,
  170. shape.points.map((pt) => vec.rotWith(pt, c, shape.rotation))
  171. )
  172. })
  173. return (
  174. boundsContain(brushBounds, rBounds) ||
  175. intersectPolylineBounds(
  176. rotatedBounds,
  177. translateBounds(brushBounds, vec.neg(shape.point))
  178. ).length > 0
  179. )
  180. },
  181. transform(shape, bounds, { initialShape, scaleX, scaleY }) {
  182. const initialShapeBounds = getFromCache(
  183. this.boundsCache,
  184. initialShape,
  185. (cache) => {
  186. cache.set(shape, getBoundsFromPoints(initialShape.points))
  187. }
  188. )
  189. shape.points = initialShape.points.map(([x, y, r]) => {
  190. return [
  191. bounds.width *
  192. (scaleX < 0 // * sin?
  193. ? 1 - x / initialShapeBounds.width
  194. : x / initialShapeBounds.width),
  195. bounds.height *
  196. (scaleY < 0 // * cos?
  197. ? 1 - y / initialShapeBounds.height
  198. : y / initialShapeBounds.height),
  199. r,
  200. ]
  201. })
  202. const newBounds = getBoundsFromPoints(shape.points)
  203. shape.point = vec.sub(
  204. [bounds.minX, bounds.minY],
  205. [newBounds.minX, newBounds.minY]
  206. )
  207. return this
  208. },
  209. // applyStyles(shape, style) {
  210. // const styles = { ...shape.style, ...style }
  211. // styles.dash = DashStyle.Solid
  212. // shape.style = styles
  213. // return this
  214. // },
  215. onSessionComplete(shape) {
  216. const bounds = this.getBounds(shape)
  217. const [x1, y1] = vec.sub([bounds.minX, bounds.minY], shape.point)
  218. shape.points = shape.points.map(([x0, y0, p]) => [x0 - x1, y0 - y1, p])
  219. this.translateTo(shape, vec.add(shape.point, [x1, y1]))
  220. return this
  221. },
  222. })
  223. export default draw
  224. const simulatePressureSettings = {
  225. simulatePressure: true,
  226. }
  227. const realPressureSettings = {
  228. easing: (t: number) => t * t,
  229. simulatePressure: false,
  230. start: { taper: 1 },
  231. end: { taper: 1 },
  232. }
  233. /**
  234. * Get the fill path for a closed draw shape.
  235. *
  236. * ### Example
  237. *
  238. *```ts
  239. * someCache.set(getFillPath(shape))
  240. *```
  241. */
  242. function getFillPath(shape: DrawShape) {
  243. const styles = getShapeStyle(shape.style)
  244. if (shape.points.length < 2) {
  245. return ''
  246. }
  247. return getSvgPathFromStroke(
  248. getStrokePoints(shape.points, {
  249. size: 1 + +styles.strokeWidth * 2,
  250. thinning: 0.85,
  251. end: { taper: +styles.strokeWidth * 20 },
  252. start: { taper: +styles.strokeWidth * 20 },
  253. }).map((pt) => pt.point)
  254. )
  255. }
  256. /**
  257. * Get the path data for a draw stroke.
  258. *
  259. * ### Example
  260. *
  261. *```ts
  262. * someCache.set(getDrawStrokePath(shape))
  263. *```
  264. */
  265. function getDrawStrokePath(shape: DrawShape) {
  266. const styles = getShapeStyle(shape.style)
  267. if (shape.points.length < 2) {
  268. return ''
  269. }
  270. const options =
  271. shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings
  272. const stroke = getStroke(shape.points, {
  273. size: 1 + +styles.strokeWidth * 2,
  274. thinning: 0.85,
  275. end: { taper: +styles.strokeWidth * 10 },
  276. start: { taper: +styles.strokeWidth * 10 },
  277. ...options,
  278. })
  279. return getSvgPathFromStroke(stroke)
  280. }
  281. function getSolidStrokePath(shape: DrawShape) {
  282. let { points } = shape
  283. let len = points.length
  284. if (len === 0) return 'M 0 0 L 0 0'
  285. if (len < 3) return `M ${points[0][0]} ${points[0][1]}`
  286. points = getStrokePoints(points).map((pt) => pt.point)
  287. len = points.length
  288. const d = points.reduce(
  289. (acc, [x0, y0], i, arr) => {
  290. if (i === len - 1) {
  291. acc.push('L', x0, y0)
  292. return acc
  293. }
  294. const [x1, y1] = arr[i + 1]
  295. acc.push(
  296. x0.toFixed(2),
  297. y0.toFixed(2),
  298. ((x0 + x1) / 2).toFixed(2),
  299. ((y0 + y1) / 2).toFixed(2)
  300. )
  301. return acc
  302. },
  303. ['M', points[0][0], points[0][1], 'Q']
  304. )
  305. const path = d.join(' ').replaceAll(/(\s[0-9]*\.[0-9]{2})([0-9]*)\b/g, '$1')
  306. return path
  307. }
  308. // /**
  309. // * Get the path data for a solid draw stroke.
  310. // *
  311. // * ### Example
  312. // *
  313. // *```ts
  314. // * getSolidStrokePath(shape)
  315. // *```
  316. // */
  317. // function getSolidDrawStrokePath(shape: DrawShape) {
  318. // const styles = getShapeStyle(shape.style)
  319. // if (shape.points.length < 2) {
  320. // return ''
  321. // }
  322. // const options =
  323. // shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings
  324. // const stroke = getStroke(shape.points, {
  325. // size: 1 + +styles.strokeWidth * 2,
  326. // thinning: 0,
  327. // end: { taper: +styles.strokeWidth * 10 },
  328. // start: { taper: +styles.strokeWidth * 10 },
  329. // ...options,
  330. // })
  331. // return getSvgPathFromStroke(stroke)
  332. // }