feat: 形状支持自定义绘制线条(#234)

This commit is contained in:
pipipi-pikachu 2023-12-17 16:26:15 +08:00
parent 23fc06462b
commit 40778513d9
7 changed files with 83 additions and 34 deletions

View File

@ -109,6 +109,7 @@ npm run serve
- 设置为背景图 - 设置为背景图
#### 形状 #### 形状
- 绘制任意多边形 - 绘制任意多边形
- 绘制任意线条(未封闭形状模拟)
- 替换形状 - 替换形状
- 填充色 - 填充色
- 边框 - 边框

View File

@ -92,7 +92,7 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
.message { .message {
max-width: 500px; max-width: 600px;
& + & { & + & {
margin-top: 15px; margin-top: 15px;

View File

@ -86,6 +86,7 @@ export const HOTKEY_DOC = [
{ label: '创建水平 / 垂直线条', value: '按住 Ctrl 或 Shift' }, { label: '创建水平 / 垂直线条', value: '按住 Ctrl 或 Shift' },
{ label: '切换焦点元素', value: 'Tab' }, { label: '切换焦点元素', value: 'Tab' },
{ label: '确认图片裁剪', value: 'Enter' }, { label: '确认图片裁剪', value: 'Enter' },
{ label: '完成自定义形状绘制', value: 'Enter' },
], ],
}, },
{ {

View File

@ -213,7 +213,7 @@ export default () => {
* @param position * @param position
* @param data * @param data
*/ */
const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem) => { const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem, supplement: Partial<PPTShapeElement> = {}) => {
const { left, top, width, height } = position const { left, top, width, height } = position
const newElement: PPTShapeElement = { const newElement: PPTShapeElement = {
type: 'shape', type: 'shape',
@ -227,6 +227,7 @@ export default () => {
fill: theme.value.themeColor, fill: theme.value.themeColor,
fixedRatio: false, fixedRatio: false,
rotate: 0, rotate: 0,
...supplement,
} }
if (data.special) newElement.special = true if (data.special) newElement.special = true
if (data.pathFormula) { if (data.pathFormula) {

View File

@ -83,6 +83,8 @@ export interface CreateCustomShapeData {
end: [number, number] end: [number, number]
path: string path: string
viewBox: [number, number] viewBox: [number, number]
fill?: string
outline?: PPTElementOutline
} }
export interface CreatingTextElement { export interface CreatingTextElement {

View File

@ -20,7 +20,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useKeyboardStore, useMainStore } from '@/store' import { useKeyboardStore, useMainStore, useSlidesStore } from '@/store'
import type { CreateCustomShapeData } from '@/types/edit' import type { CreateCustomShapeData } from '@/types/edit'
import { KEYS } from '@/configs/hotkey' import { KEYS } from '@/configs/hotkey'
import message from '@/utils/message' import message from '@/utils/message'
@ -30,8 +30,10 @@ const emit = defineEmits<{
}>() }>()
const mainStore = useMainStore() const mainStore = useMainStore()
const { ctrlOrShiftKeyActive } = storeToRefs(useKeyboardStore()) const { ctrlOrShiftKeyActive } = storeToRefs(useKeyboardStore())
const { theme } = storeToRefs(useSlidesStore())
const shapeCanvasRef = ref<HTMLElement>() const shapeCanvasRef = ref<HTMLElement>()
const isMouseDown = ref(false)
const offset = ref({ const offset = ref({
x: 0, x: 0,
y: 0, y: 0,
@ -42,14 +44,16 @@ onMounted(() => {
offset.value = { x, y } offset.value = { x, y }
}) })
const mousePosition = ref<[number, number]>() const mousePosition = ref<[number, number] | null>(null)
const points = ref<[number, number][]>([]) const points = ref<[number, number][]>([])
const closed = ref(false) const closed = ref(false)
const getPoint = (e: MouseEvent) => { const getPoint = (e: MouseEvent, custom = false) => {
let pageX = e.pageX - offset.value.x let pageX = e.pageX - offset.value.x
let pageY = e.pageY - offset.value.y let pageY = e.pageY - offset.value.y
if (custom) return { pageX, pageY }
if (ctrlOrShiftKeyActive.value && points.value.length) { if (ctrlOrShiftKeyActive.value && points.value.length) {
const [lastPointX, lastPointY] = points.value[points.value.length - 1] const [lastPointX, lastPointY] = points.value[points.value.length - 1]
if (Math.abs(lastPointX - pageX) - Math.abs(lastPointY - pageY) > 0) { if (Math.abs(lastPointX - pageX) - Math.abs(lastPointY - pageY) > 0) {
@ -61,6 +65,13 @@ const getPoint = (e: MouseEvent) => {
} }
const updateMousePosition = (e: MouseEvent) => { 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) const { pageX, pageY } = getPoint(e)
mousePosition.value = [pageX, pageY] mousePosition.value = [pageX, pageY]
@ -87,10 +98,7 @@ const path = computed(() => {
return d return d
}) })
const addPoint = (e: MouseEvent) => { const getCreateData = (close = true) => {
const { pageX, pageY } = getPoint(e)
if (closed.value) {
const xList = points.value.map(item => item[0]) const xList = points.value.map(item => item[0])
const yList = points.value.map(item => item[1]) const yList = points.value.map(item => item[1])
const minX = Math.min(...xList) const minX = Math.min(...xList)
@ -101,34 +109,65 @@ const addPoint = (e: MouseEvent) => {
const formatedPoints = points.value.map(point => { const formatedPoints = points.value.map(point => {
return [point[0] - minX, point[1] - minY] return [point[0] - minX, point[1] - minY]
}) })
let path = '' let path = ''
for (let i = 0; i < formatedPoints.length; i++) { for (let i = 0; i < formatedPoints.length; i++) {
const point = formatedPoints[i] const point = formatedPoints[i]
if (i === 0) path += `M ${point[0]} ${point[1]} ` if (i === 0) path += `M ${point[0]} ${point[1]} `
else path += `L ${point[0]} ${point[1]} ` else path += `L ${point[0]} ${point[1]} `
} }
path += 'Z' if (close) path += 'Z'
emit('created', { const start: [number, number] = [minX + offset.value.x, minY + offset.value.y]
start: [minX + offset.value.x, minY + offset.value.y], const end: [number, number] = [maxX + offset.value.x, maxY + offset.value.y]
end: [maxX + offset.value.x, maxY + offset.value.y], const viewBox: [number, number] = [maxX - minX, maxY - minY]
return {
start,
end,
path, path,
viewBox: [maxX - minX, maxY - minY], 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]) else points.value.push([pageX, pageY])
document.onmouseup = () => {
isMouseDown.value = false
}
} }
const close = () => { const close = () => {
mainStore.setCreatingCustomShapeState(false) mainStore.setCreatingCustomShapeState(false)
} }
const create = () => {
emit('created', {
...getCreateData(false),
fill: 'rgba(0, 0, 0, 0)',
outline: {
width: 2,
color: theme.value.themeColor,
style: 'solid',
},
})
close()
}
const keydownListener = (e: KeyboardEvent) => { const keydownListener = (e: KeyboardEvent) => {
const key = e.key.toUpperCase() const key = e.key.toUpperCase()
if (key === KEYS.ESC) close() if (key === KEYS.ESC) close()
if (key === KEYS.ENTER) create()
} }
onMounted(() => { onMounted(() => {
message.success('点击开始绘制任意多边形,首尾闭合完成绘制,按 ESC 键或鼠标右键关闭') message.success('点击绘制任意形状,首尾闭合完成绘制,按 ESC 键或鼠标右键取消,按 ENTER 键提前完成', {
duration: 5000,
})
document.addEventListener('keydown', keydownListener) document.addEventListener('keydown', keydownListener)
}) })
onUnmounted(() => document.removeEventListener('keydown', keydownListener)) onUnmounted(() => document.removeEventListener('keydown', keydownListener))

View File

@ -102,7 +102,7 @@ import { throttle } from 'lodash'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store' import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
import type { ContextmenuItem } from '@/components/Contextmenu/types' import type { ContextmenuItem } from '@/components/Contextmenu/types'
import type { PPTElement } from '@/types/slides' import type { PPTElement, PPTShapeElement } from '@/types/slides'
import type { AlignmentLineProps, CreateCustomShapeData } from '@/types/edit' import type { AlignmentLineProps, CreateCustomShapeData } from '@/types/edit'
import { injectKeySlideScale } from '@/types/injectKey' import { injectKeySlideScale } from '@/types/injectKey'
import { removeAllRanges } from '@/utils/selection' import { removeAllRanges } from '@/utils/selection'
@ -277,7 +277,12 @@ const insertCustomShape = (data: CreateCustomShapeData) => {
viewBox, viewBox,
} = data } = data
const position = formatCreateSelection({ start, end }) const position = formatCreateSelection({ start, end })
position && createShapeElement(position, { path, viewBox }) if (position) {
const supplement: Partial<PPTShapeElement> = {}
if (data.fill) supplement.fill = data.fill
if (data.outline) supplement.outline = data.outline
createShapeElement(position, { path, viewBox }, supplement)
}
mainStore.setCreatingCustomShapeState(false) mainStore.setCreatingCustomShapeState(false)
} }