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

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