ShapeCreateCanvas.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <template>
  2. <div
  3. class="shape-create-canvas"
  4. ref="shapeCanvasRef"
  5. @mousedown.stop="$event => addPoint($event)"
  6. @mousemove="$event => updateMousePosition($event)"
  7. @contextmenu.stop.prevent="close()"
  8. >
  9. <svg overflow="visible">
  10. <path
  11. :d="path"
  12. stroke="#5b9bd5"
  13. :fill="closed ? 'rgba(226, 83, 77, 0.15)' : 'none'"
  14. stroke-width="2"
  15. ></path>
  16. </svg>
  17. </div>
  18. </template>
  19. <script lang="ts" setup>
  20. import { computed, onMounted, onUnmounted, ref } from 'vue'
  21. import { storeToRefs } from 'pinia'
  22. import { useKeyboardStore, useMainStore, useSlidesStore } from '@/store'
  23. import type { CreateCustomShapeData } from '@/types/edit'
  24. import { KEYS } from '@/configs/hotkey'
  25. import message from '@/utils/message'
  26. const emit = defineEmits<{
  27. (event: 'created', payload: CreateCustomShapeData): void
  28. }>()
  29. const mainStore = useMainStore()
  30. const { ctrlOrShiftKeyActive } = storeToRefs(useKeyboardStore())
  31. const { theme } = storeToRefs(useSlidesStore())
  32. const shapeCanvasRef = ref<HTMLElement>()
  33. const isMouseDown = ref(false)
  34. const offset = ref({
  35. x: 0,
  36. y: 0,
  37. })
  38. onMounted(() => {
  39. if (!shapeCanvasRef.value) return
  40. const { x, y } = shapeCanvasRef.value.getBoundingClientRect()
  41. offset.value = { x, y }
  42. })
  43. const mousePosition = ref<[number, number] | null>(null)
  44. const points = ref<[number, number][]>([])
  45. const closed = ref(false)
  46. const getPoint = (e: MouseEvent, custom = false) => {
  47. let pageX = e.pageX - offset.value.x
  48. let pageY = e.pageY - offset.value.y
  49. if (custom) return { pageX, pageY }
  50. if (ctrlOrShiftKeyActive.value && points.value.length) {
  51. const [lastPointX, lastPointY] = points.value[points.value.length - 1]
  52. if (Math.abs(lastPointX - pageX) - Math.abs(lastPointY - pageY) > 0) {
  53. pageY = lastPointY
  54. }
  55. else pageX = lastPointX
  56. }
  57. return { pageX, pageY }
  58. }
  59. const updateMousePosition = (e: MouseEvent) => {
  60. if (isMouseDown.value) {
  61. const { pageX, pageY } = getPoint(e, true)
  62. points.value.push([pageX, pageY])
  63. mousePosition.value = null
  64. return
  65. }
  66. const { pageX, pageY } = getPoint(e)
  67. mousePosition.value = [pageX, pageY]
  68. if (points.value.length >= 2) {
  69. const [firstPointX, firstPointY] = points.value[0]
  70. if (Math.abs(firstPointX - pageX) < 5 && Math.abs(firstPointY - pageY) < 5) {
  71. closed.value = true
  72. }
  73. else closed.value = false
  74. }
  75. else closed.value = false
  76. }
  77. const path = computed(() => {
  78. let d = ''
  79. for (let i = 0; i < points.value.length; i++) {
  80. const point = points.value[i]
  81. if (i === 0) d += `M ${point[0]} ${point[1]} `
  82. else d += `L ${point[0]} ${point[1]} `
  83. }
  84. if (points.value.length && mousePosition.value) {
  85. d += `L ${mousePosition.value[0]} ${mousePosition.value[1]}`
  86. }
  87. return d
  88. })
  89. const getCreateData = (close = true) => {
  90. const xList = points.value.map(item => item[0])
  91. const yList = points.value.map(item => item[1])
  92. const minX = Math.min(...xList)
  93. const minY = Math.min(...yList)
  94. const maxX = Math.max(...xList)
  95. const maxY = Math.max(...yList)
  96. const formatedPoints = points.value.map(point => {
  97. return [point[0] - minX, point[1] - minY]
  98. })
  99. let path = ''
  100. for (let i = 0; i < formatedPoints.length; i++) {
  101. const point = formatedPoints[i]
  102. if (i === 0) path += `M ${point[0]} ${point[1]} `
  103. else path += `L ${point[0]} ${point[1]} `
  104. }
  105. if (close) path += 'Z'
  106. const start: [number, number] = [minX + offset.value.x, minY + offset.value.y]
  107. const end: [number, number] = [maxX + offset.value.x, maxY + offset.value.y]
  108. const viewBox: [number, number] = [maxX - minX, maxY - minY]
  109. return {
  110. start,
  111. end,
  112. path,
  113. viewBox,
  114. }
  115. }
  116. const addPoint = (e: MouseEvent) => {
  117. const { pageX, pageY } = getPoint(e)
  118. isMouseDown.value = true
  119. if (closed.value) emit('created', getCreateData())
  120. else points.value.push([pageX, pageY])
  121. document.onmouseup = () => {
  122. isMouseDown.value = false
  123. }
  124. }
  125. const close = () => {
  126. mainStore.setCreatingCustomShapeState(false)
  127. }
  128. const create = () => {
  129. emit('created', {
  130. ...getCreateData(false),
  131. fill: 'rgba(0, 0, 0, 0)',
  132. outline: {
  133. width: 2,
  134. color: theme.value.themeColor,
  135. style: 'solid',
  136. },
  137. })
  138. close()
  139. }
  140. const keydownListener = (e: KeyboardEvent) => {
  141. const key = e.key.toUpperCase()
  142. if (key === KEYS.ESC) close()
  143. if (key === KEYS.ENTER) create()
  144. }
  145. onMounted(() => {
  146. message.success('点击绘制任意形状,首尾闭合完成绘制,按 ESC 键或鼠标右键取消,按 ENTER 键提前完成', {
  147. duration: 0,
  148. })
  149. document.addEventListener('keydown', keydownListener)
  150. })
  151. onUnmounted(() => {
  152. document.removeEventListener('keydown', keydownListener)
  153. message.closeAll()
  154. })
  155. </script>
  156. <style lang="scss" scoped>
  157. .shape-create-canvas {
  158. position: absolute;
  159. top: 0;
  160. left: 0;
  161. width: 100%;
  162. height: 100%;
  163. z-index: 2;
  164. cursor: crosshair;
  165. svg {
  166. width: 100%;
  167. height: 100%;
  168. overflow: visible;
  169. }
  170. }
  171. </style>