mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 形状支持自定义绘制线条(#234)
This commit is contained in:
parent
23fc06462b
commit
40778513d9
@ -109,6 +109,7 @@ npm run serve
|
|||||||
- 设置为背景图
|
- 设置为背景图
|
||||||
#### 形状
|
#### 形状
|
||||||
- 绘制任意多边形
|
- 绘制任意多边形
|
||||||
|
- 绘制任意线条(未封闭形状模拟)
|
||||||
- 替换形状
|
- 替换形状
|
||||||
- 填充色
|
- 填充色
|
||||||
- 边框
|
- 边框
|
||||||
|
@ -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;
|
||||||
|
@ -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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user