You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ellipse.tsx 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import { uniqueId, getPerfectEllipseDashProps } from 'utils/utils'
  2. import vec from 'utils/vec'
  3. import { DashStyle, EllipseShape, ShapeType } from 'types'
  4. import { getShapeUtils } from './index'
  5. import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds'
  6. import { intersectEllipseBounds } from 'utils/intersections'
  7. import { pointInEllipse } from 'utils/hitTests'
  8. import { ease, getSvgPathFromStroke, rng, translateBounds } from 'utils/utils'
  9. import { defaultStyle, getShapeStyle } from 'state/shape-styles'
  10. import getStroke from 'perfect-freehand'
  11. import { registerShapeUtils } from './register'
  12. const pathCache = new WeakMap<EllipseShape, string>([])
  13. const ellipse = registerShapeUtils<EllipseShape>({
  14. boundsCache: new WeakMap([]),
  15. create(props) {
  16. return {
  17. id: uniqueId(),
  18. seed: Math.random(),
  19. type: ShapeType.Ellipse,
  20. isGenerated: false,
  21. name: 'Ellipse',
  22. parentId: 'page1',
  23. childIndex: 0,
  24. point: [0, 0],
  25. radiusX: 1,
  26. radiusY: 1,
  27. rotation: 0,
  28. isAspectRatioLocked: false,
  29. isLocked: false,
  30. isHidden: false,
  31. style: defaultStyle,
  32. ...props,
  33. }
  34. },
  35. render(shape) {
  36. const { id, radiusX, radiusY, style } = shape
  37. const styles = getShapeStyle(style)
  38. const strokeWidth = +styles.strokeWidth
  39. if (style.dash === DashStyle.Solid) {
  40. if (!pathCache.has(shape)) {
  41. renderPath(shape)
  42. }
  43. const path = pathCache.get(shape)
  44. return (
  45. <g id={id}>
  46. <ellipse
  47. id={id}
  48. cx={radiusX}
  49. cy={radiusY}
  50. rx={Math.max(0, radiusX - strokeWidth / 2)}
  51. ry={Math.max(0, radiusY - strokeWidth / 2)}
  52. stroke="none"
  53. />
  54. <path d={path} fill={styles.stroke} />
  55. </g>
  56. )
  57. }
  58. const rx = Math.max(0, radiusX - strokeWidth / 2)
  59. const ry = Math.max(0, radiusY - strokeWidth / 2)
  60. const { strokeDasharray, strokeDashoffset } = getPerfectEllipseDashProps(
  61. rx,
  62. ry,
  63. strokeWidth,
  64. shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed'
  65. )
  66. return (
  67. <g id={id}>
  68. <ellipse
  69. id={id}
  70. cx={radiusX}
  71. cy={radiusY}
  72. rx={rx}
  73. ry={ry}
  74. fill={styles.fill}
  75. stroke={styles.stroke}
  76. strokeDasharray={strokeDasharray}
  77. strokeDashoffset={strokeDashoffset}
  78. />
  79. </g>
  80. )
  81. },
  82. getBounds(shape) {
  83. if (!this.boundsCache.has(shape)) {
  84. const { radiusX, radiusY } = shape
  85. const bounds = {
  86. minX: 0,
  87. minY: 0,
  88. maxX: radiusX * 2,
  89. maxY: radiusY * 2,
  90. width: radiusX * 2,
  91. height: radiusY * 2,
  92. }
  93. this.boundsCache.set(shape, bounds)
  94. }
  95. return translateBounds(this.boundsCache.get(shape), shape.point)
  96. },
  97. getRotatedBounds(shape) {
  98. return getRotatedEllipseBounds(
  99. shape.point[0],
  100. shape.point[1],
  101. shape.radiusX,
  102. shape.radiusY,
  103. shape.rotation
  104. )
  105. },
  106. getCenter(shape) {
  107. return [shape.point[0] + shape.radiusX, shape.point[1] + shape.radiusY]
  108. },
  109. hitTest(shape, point) {
  110. return pointInEllipse(
  111. point,
  112. vec.add(shape.point, [shape.radiusX, shape.radiusY]),
  113. shape.radiusX,
  114. shape.radiusY,
  115. shape.rotation
  116. )
  117. },
  118. hitTestBounds(this, shape, brushBounds) {
  119. const shapeBounds = this.getBounds(shape)
  120. return (
  121. boundsContained(shapeBounds, brushBounds) ||
  122. intersectEllipseBounds(
  123. vec.add(shape.point, [shape.radiusX, shape.radiusY]),
  124. shape.radiusX,
  125. shape.radiusY,
  126. brushBounds,
  127. shape.rotation
  128. ).length > 0
  129. )
  130. },
  131. transform(shape, bounds, { scaleX, scaleY, initialShape }) {
  132. // TODO: Locked aspect ratio transform
  133. shape.point = [bounds.minX, bounds.minY]
  134. shape.radiusX = bounds.width / 2
  135. shape.radiusY = bounds.height / 2
  136. shape.rotation =
  137. (scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
  138. ? -initialShape.rotation
  139. : initialShape.rotation
  140. return this
  141. },
  142. transformSingle(shape, bounds, info) {
  143. return this.transform(shape, bounds, info)
  144. },
  145. })
  146. export default ellipse
  147. function renderPath(shape: EllipseShape) {
  148. const { style, id, radiusX, radiusY, point } = shape
  149. const getRandom = rng(id)
  150. const center = vec.sub(getShapeUtils(shape).getCenter(shape), point)
  151. const strokeWidth = +getShapeStyle(style).strokeWidth
  152. const rx = radiusX + getRandom() * strokeWidth
  153. const ry = radiusY + getRandom() * strokeWidth
  154. const points: number[][] = []
  155. const start = Math.PI + Math.PI * getRandom()
  156. const overlap = Math.PI / 12
  157. for (let i = 2; i < 8; i++) {
  158. const rads = start + overlap * 2 * (i / 8)
  159. const x = rx * Math.cos(rads) + center[0]
  160. const y = ry * Math.sin(rads) + center[1]
  161. points.push([x, y])
  162. }
  163. for (let i = 5; i < 32; i++) {
  164. const rads = start + overlap * 2 + Math.PI * 2.5 * ease(i / 35)
  165. const x = rx * Math.cos(rads) + center[0]
  166. const y = ry * Math.sin(rads) + center[1]
  167. points.push([x, y])
  168. }
  169. for (let i = 0; i < 8; i++) {
  170. const rads = start + overlap * 2 * (i / 4)
  171. const x = rx * Math.cos(rads) + center[0]
  172. const y = ry * Math.sin(rads) + center[1]
  173. points.push([x, y])
  174. }
  175. const stroke = getStroke(points, {
  176. size: 1 + strokeWidth,
  177. thinning: 0.6,
  178. easing: (t) => t * t * t * t,
  179. end: { taper: strokeWidth * 20 },
  180. start: { taper: strokeWidth * 20 },
  181. simulatePressure: false,
  182. })
  183. pathCache.set(shape, getSvgPathFromStroke(stroke))
  184. }