| <template> | |
| <div | |
| class="shape-create-canvas" | |
| ref="shapeCanvasRef" | |
| @mousedown.stop="$event => addPoint($event)" | |
| @mousemove="$event => updateMousePosition($event)" | |
| @contextmenu.stop.prevent="close()" | |
| > | |
| <svg overflow="visible"> | |
| <path | |
| :d="path" | |
| stroke="#d14424" | |
| :fill="closed ? 'rgba(226, 83, 77, 0.15)' : 'none'" | |
| stroke-width="2" | |
| ></path> | |
| </svg> | |
| </div> | |
| </template> | |
| <script lang="ts" setup> | |
| import { computed, onMounted, onUnmounted, ref } from 'vue' | |
| import { storeToRefs } from 'pinia' | |
| import { useKeyboardStore, useMainStore, useSlidesStore } from '@/store' | |
| import type { CreateCustomShapeData } from '@/types/edit' | |
| import { KEYS } from '@/configs/hotkey' | |
| import message from '@/utils/message' | |
| const emit = defineEmits<{ | |
| (event: 'created', payload: CreateCustomShapeData): void | |
| }>() | |
| const mainStore = useMainStore() | |
| const { ctrlOrShiftKeyActive } = storeToRefs(useKeyboardStore()) | |
| const { theme } = storeToRefs(useSlidesStore()) | |
| const shapeCanvasRef = ref<HTMLElement>() | |
| const isMouseDown = ref(false) | |
| const offset = ref({ | |
| x: 0, | |
| y: 0, | |
| }) | |
| onMounted(() => { | |
| if (!shapeCanvasRef.value) return | |
| const { x, y } = shapeCanvasRef.value.getBoundingClientRect() | |
| offset.value = { x, y } | |
| }) | |
| const mousePosition = ref<[number, number] | null>(null) | |
| const points = ref<[number, number][]>([]) | |
| const closed = ref(false) | |
| const getPoint = (e: MouseEvent, custom = false) => { | |
| let pageX = e.pageX - offset.value.x | |
| let pageY = e.pageY - offset.value.y | |
| if (custom) return { pageX, pageY } | |
| if (ctrlOrShiftKeyActive.value && points.value.length) { | |
| const [lastPointX, lastPointY] = points.value[points.value.length - 1] | |
| if (Math.abs(lastPointX - pageX) - Math.abs(lastPointY - pageY) > 0) { | |
| pageY = lastPointY | |
| } | |
| else pageX = lastPointX | |
| } | |
| return { pageX, pageY } | |
| } | |
| const updateMousePosition = (e: MouseEvent) => { | |
| if (isMouseDown.value) { | |
| const { pageX, pageY } = getPoint(e, true) | |
| points.value.push([pageX, pageY]) | |
| mousePosition.value = null | |
| return | |
| } | |
| const { pageX, pageY } = getPoint(e) | |
| mousePosition.value = [pageX, pageY] | |
| if (points.value.length >= 2) { | |
| const [firstPointX, firstPointY] = points.value[0] | |
| if (Math.abs(firstPointX - pageX) < 5 && Math.abs(firstPointY - pageY) < 5) { | |
| closed.value = true | |
| } | |
| else closed.value = false | |
| } | |
| else closed.value = false | |
| } | |
| const path = computed(() => { | |
| let d = '' | |
| for (let i = 0; i < points.value.length; i++) { | |
| const point = points.value[i] | |
| if (i === 0) d += `M ${point[0]} ${point[1]} ` | |
| else d += `L ${point[0]} ${point[1]} ` | |
| } | |
| if (points.value.length && mousePosition.value) { | |
| d += `L ${mousePosition.value[0]} ${mousePosition.value[1]}` | |
| } | |
| return d | |
| }) | |
| const getCreateData = (close = true) => { | |
| const xList = points.value.map(item => item[0]) | |
| const yList = points.value.map(item => item[1]) | |
| const minX = Math.min(...xList) | |
| const minY = Math.min(...yList) | |
| const maxX = Math.max(...xList) | |
| const maxY = Math.max(...yList) | |
| const formatedPoints = points.value.map(point => { | |
| return [point[0] - minX, point[1] - minY] | |
| }) | |
| let path = '' | |
| for (let i = 0; i < formatedPoints.length; i++) { | |
| const point = formatedPoints[i] | |
| if (i === 0) path += `M ${point[0]} ${point[1]} ` | |
| else path += `L ${point[0]} ${point[1]} ` | |
| } | |
| if (close) path += 'Z' | |
| const start: [number, number] = [minX + offset.value.x, minY + offset.value.y] | |
| const end: [number, number] = [maxX + offset.value.x, maxY + offset.value.y] | |
| const viewBox: [number, number] = [maxX - minX, maxY - minY] | |
| return { | |
| start, | |
| end, | |
| path, | |
| viewBox, | |
| } | |
| } | |
| const addPoint = (e: MouseEvent) => { | |
| const { pageX, pageY } = getPoint(e) | |
| isMouseDown.value = true | |
| if (closed.value) emit('created', getCreateData()) | |
| else points.value.push([pageX, pageY]) | |
| document.onmouseup = () => { | |
| isMouseDown.value = false | |
| } | |
| } | |
| const close = () => { | |
| mainStore.setCreatingCustomShapeState(false) | |
| } | |
| const create = () => { | |
| emit('created', { | |
| ...getCreateData(false), | |
| fill: 'rgba(0, 0, 0, 0)', | |
| outline: { | |
| width: 2, | |
| color: theme.value.themeColors[0], | |
| style: 'solid', | |
| }, | |
| }) | |
| close() | |
| } | |
| const keydownListener = (e: KeyboardEvent) => { | |
| const key = e.key.toUpperCase() | |
| if (key === KEYS.ESC) close() | |
| if (key === KEYS.ENTER) create() | |
| } | |
| onMounted(() => { | |
| message.success('点击绘制任意形状,首尾闭合完成绘制,按 ESC 键或鼠标右键取消,按 ENTER 键提前完成', { | |
| duration: 0, | |
| }) | |
| document.addEventListener('keydown', keydownListener) | |
| }) | |
| onUnmounted(() => { | |
| document.removeEventListener('keydown', keydownListener) | |
| message.closeAll() | |
| }) | |
| </script> | |
| <style lang="scss" scoped> | |
| .shape-create-canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 2; | |
| cursor: crosshair; | |
| svg { | |
| width: 100%; | |
| height: 100%; | |
| overflow: visible; | |
| } | |
| } | |
| </style> |