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.7KB

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