選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

ellipse.tsx 5.0KB

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