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

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