Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

ellipse.tsx 5.6KB

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