mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
update
This commit is contained in:
parent
9c63766314
commit
071c608fcf
@ -15,17 +15,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
|
||||
interface Axis {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface AlignmentLineProps {
|
||||
type: 'vertical' | 'horizontal';
|
||||
axis: Axis;
|
||||
length: number;
|
||||
}
|
||||
import { AlignmentLineAxis } from './types/index'
|
||||
|
||||
export default {
|
||||
name: 'alignment-line',
|
||||
@ -35,7 +25,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
axis: {
|
||||
type: Object as PropType<Axis>,
|
||||
type: Object as PropType<AlignmentLineAxis>,
|
||||
required: true,
|
||||
},
|
||||
length: {
|
||||
|
@ -32,12 +32,7 @@ import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes
|
||||
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
|
||||
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
|
||||
|
||||
export interface MultiSelectRange {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
}
|
||||
import { MultiSelectRange } from './types/index'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'multi-select-operate',
|
||||
|
54
src/views/Editor/Canvas/hooks/useAlignElementToCanvas.ts
Normal file
54
src/views/Editor/Canvas/hooks/useAlignElementToCanvas.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit'
|
||||
import { getElementListRange } from '../utils/elementRange'
|
||||
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||
|
||||
export default (elementList: Ref<PPTElement[]>, activeElementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
const alignElementToCanvas = (command: ElementAlignCommand) => {
|
||||
const viewportWidth = VIEWPORT_SIZE
|
||||
const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
|
||||
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
|
||||
|
||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
|
||||
for(const element of newElementList) {
|
||||
if(!activeElementIdList.value.includes(element.elId)) continue
|
||||
|
||||
if(command === ElementAlignCommands.TOP) {
|
||||
const offsetY = minY - 0
|
||||
element.top = element.top - offsetY
|
||||
}
|
||||
else if(command === ElementAlignCommands.VERTICAL) {
|
||||
const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
|
||||
element.top = element.top - offsetY
|
||||
}
|
||||
else if(command === ElementAlignCommands.BOTTOM) {
|
||||
const offsetY = maxY - viewportHeight
|
||||
element.top = element.top - offsetY
|
||||
}
|
||||
|
||||
else if(command === ElementAlignCommands.LEFT) {
|
||||
const offsetX = minX - 0
|
||||
element.left = element.left - offsetX
|
||||
}
|
||||
else if(command === ElementAlignCommands.HORIZONTAL) {
|
||||
const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
|
||||
element.left = element.left - offsetX
|
||||
}
|
||||
else if(command === ElementAlignCommands.RIGHT) {
|
||||
const offsetX = maxX - viewportWidth
|
||||
element.left = element.left - offsetX
|
||||
}
|
||||
}
|
||||
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
|
||||
}
|
||||
|
||||
return {
|
||||
alignElementToCanvas,
|
||||
}
|
||||
}
|
53
src/views/Editor/Canvas/hooks/useCombineElement.ts
Normal file
53
src/views/Editor/Canvas/hooks/useCombineElement.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
import { createRandomCode } from '@/utils/common'
|
||||
|
||||
export default (elementList: Ref<PPTElement[]>, activeElementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
// 组合元素(为当前所有激活元素添加一个相同的groupId)
|
||||
const combineElements = () => {
|
||||
if(!activeElementList.value.length) return null
|
||||
|
||||
let newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
const groupId = createRandomCode()
|
||||
|
||||
const combineElementList: PPTElement[] = []
|
||||
for(const element of newElementList) {
|
||||
if(activeElementIdList.value.includes(element.elId)) {
|
||||
element.groupId = groupId
|
||||
combineElementList.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
// 注意,组合元素的层级应该是连续的,所以需要获取该组元素中最顶层的元素,将组内其他成员从原位置移动到最顶层的元素的下面
|
||||
const combineElementMaxIndex = newElementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
|
||||
const combineElementIdList = combineElementList.map(_element => _element.elId)
|
||||
newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.elId))
|
||||
|
||||
const insertIndex = combineElementMaxIndex - combineElementList.length + 1
|
||||
newElementList.splice(insertIndex, 0, ...combineElementList)
|
||||
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
|
||||
}
|
||||
|
||||
// 取消组合元素(移除所有被激活元素的groupId)
|
||||
const uncombineElements = () => {
|
||||
if(!activeElementList.value.length) return null
|
||||
const hasElementInGroup = activeElementList.value.some(item => item.groupId)
|
||||
if(!hasElementInGroup) return null
|
||||
|
||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
for(const element of newElementList) {
|
||||
if(activeElementIdList.value.includes(element.elId) && element.groupId) delete element.groupId
|
||||
}
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
|
||||
}
|
||||
|
||||
return {
|
||||
combineElements,
|
||||
uncombineElements,
|
||||
}
|
||||
}
|
49
src/views/Editor/Canvas/hooks/useCopyAndPasteElement.ts
Normal file
49
src/views/Editor/Canvas/hooks/useCopyAndPasteElement.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
import { copyText, readClipboard } from '@/utils/clipboard'
|
||||
import { encrypt, decrypt } from '@/utils/crypto'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
export default (deleteElement: () => void, activeElementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
const copyElement = () => {
|
||||
if(!activeElementIdList.value.length) return
|
||||
|
||||
const text = encrypt(JSON.stringify({
|
||||
type: 'elements',
|
||||
data: activeElementList.value,
|
||||
}))
|
||||
|
||||
copyText(text).then(() => {
|
||||
store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
|
||||
message.success('元素已复制到剪贴板', 0.8)
|
||||
})
|
||||
}
|
||||
|
||||
const cutElement = () => {
|
||||
copyElement()
|
||||
deleteElement()
|
||||
}
|
||||
|
||||
const pasteElement = () => {
|
||||
readClipboard().then(text => {
|
||||
let clipboardData
|
||||
try {
|
||||
clipboardData = JSON.parse(decrypt(text))
|
||||
}
|
||||
catch {
|
||||
clipboardData = text
|
||||
}
|
||||
console.log(clipboardData)
|
||||
}).catch(err => message.warning(err))
|
||||
}
|
||||
|
||||
return {
|
||||
copyElement,
|
||||
cutElement,
|
||||
pasteElement,
|
||||
}
|
||||
}
|
26
src/views/Editor/Canvas/hooks/useDeleteElement.ts
Normal file
26
src/views/Editor/Canvas/hooks/useDeleteElement.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
|
||||
export default (elementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
const deleteElement = () => {
|
||||
if(!activeElementIdList.value.length) return
|
||||
const newElementList = elementList.value.filter(el => !activeElementIdList.value.includes(el.elId))
|
||||
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
|
||||
}
|
||||
|
||||
const deleteAllElements = () => {
|
||||
if(!elementList.value.length) return
|
||||
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: [] })
|
||||
}
|
||||
|
||||
return {
|
||||
deleteElement,
|
||||
deleteAllElements,
|
||||
}
|
||||
}
|
41
src/views/Editor/Canvas/hooks/useLockElement.ts
Normal file
41
src/views/Editor/Canvas/hooks/useLockElement.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { useStore } from 'vuex'
|
||||
import { Ref } from 'vue'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
|
||||
export default (elementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
const lockElement = (handleElement: PPTElement) => {
|
||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
|
||||
|
||||
for(const element of newElementList) {
|
||||
if(activeElementIdList.value.includes(handleElement.elId)) element.isLock = true
|
||||
}
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
|
||||
}
|
||||
|
||||
const unlockElement = (handleElement: PPTElement) => {
|
||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
|
||||
|
||||
if(handleElement.groupId) {
|
||||
for(const element of newElementList) {
|
||||
if(element.groupId === handleElement.groupId) element.isLock = false
|
||||
}
|
||||
return newElementList
|
||||
}
|
||||
|
||||
for(const element of newElementList) {
|
||||
if(element.elId === handleElement.elId) {
|
||||
element.isLock = false
|
||||
break
|
||||
}
|
||||
}
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
|
||||
}
|
||||
|
||||
return {
|
||||
lockElement,
|
||||
unlockElement,
|
||||
}
|
||||
}
|
135
src/views/Editor/Canvas/hooks/useMouseSelection.ts
Normal file
135
src/views/Editor/Canvas/hooks/useMouseSelection.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { Ref, reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
import { getElementRange } from '../utils/elementRange'
|
||||
|
||||
export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | null>, canvasScale: Ref<number>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
const mouseSelectionState = reactive({
|
||||
isShow: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
quadrant: 1,
|
||||
})
|
||||
|
||||
const updateMouseSelection = (e: MouseEvent) => {
|
||||
if(!viewportRef.value) return
|
||||
|
||||
let isMouseDown = true
|
||||
const viewportRect = viewportRef.value.getBoundingClientRect()
|
||||
|
||||
const minSelectionRange = 5
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
|
||||
const left = (startPageX - viewportRect.x) / canvasScale.value
|
||||
const top = (startPageY - viewportRect.y) / canvasScale.value
|
||||
|
||||
mouseSelectionState.isShow = false
|
||||
mouseSelectionState.quadrant = 4
|
||||
mouseSelectionState.top = top
|
||||
mouseSelectionState.left = left
|
||||
mouseSelectionState.width = 0
|
||||
mouseSelectionState.height = 0
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
const offsetWidth = (currentPageX - startPageX) / canvasScale.value
|
||||
const offsetHeight = (currentPageY - startPageY) / canvasScale.value
|
||||
|
||||
const width = Math.abs(offsetWidth)
|
||||
const height = Math.abs(offsetHeight)
|
||||
|
||||
if( width < minSelectionRange || height < minSelectionRange ) return
|
||||
|
||||
let quadrant = 0
|
||||
if( offsetWidth > 0 && offsetHeight > 0 ) quadrant = 4
|
||||
else if( offsetWidth < 0 && offsetHeight < 0 ) quadrant = 1
|
||||
else if( offsetWidth > 0 && offsetHeight < 0 ) quadrant = 2
|
||||
else if( offsetWidth < 0 && offsetHeight > 0 ) quadrant = 3
|
||||
|
||||
mouseSelectionState.isShow = true
|
||||
mouseSelectionState.quadrant = quadrant
|
||||
mouseSelectionState.width = width
|
||||
mouseSelectionState.height = height
|
||||
}
|
||||
|
||||
document.onmouseup = () => {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
isMouseDown = false
|
||||
|
||||
// 计算当前页面中的每一个元素是否处在鼠标选择范围中(必须完全包裹)
|
||||
// 将选择范围中的元素添加为激活元素
|
||||
let inRangeElementList: PPTElement[] = []
|
||||
for(let i = 0; i < elementList.value.length; i++) {
|
||||
const element = elementList.value[i]
|
||||
const mouseSelectionLeft = mouseSelectionState.left
|
||||
const mouseSelectionTop = mouseSelectionState.top
|
||||
const mouseSelectionWidth = mouseSelectionState.width
|
||||
const mouseSelectionHeight = mouseSelectionState.height
|
||||
|
||||
const quadrant = mouseSelectionState.quadrant
|
||||
|
||||
const { minX, maxX, minY, maxY } = getElementRange(element)
|
||||
|
||||
let isInclude = false
|
||||
if(quadrant === 4) {
|
||||
isInclude = minX > mouseSelectionLeft &&
|
||||
maxX < mouseSelectionLeft + mouseSelectionWidth &&
|
||||
minY > mouseSelectionTop &&
|
||||
maxY < mouseSelectionTop + mouseSelectionHeight
|
||||
}
|
||||
else if(quadrant === 1) {
|
||||
isInclude = minX > (mouseSelectionLeft - mouseSelectionWidth) &&
|
||||
maxX < (mouseSelectionLeft - mouseSelectionWidth) + mouseSelectionWidth &&
|
||||
minY > (mouseSelectionTop - mouseSelectionHeight) &&
|
||||
maxY < (mouseSelectionTop - mouseSelectionHeight) + mouseSelectionHeight
|
||||
}
|
||||
else if(quadrant === 2) {
|
||||
isInclude = minX > mouseSelectionLeft &&
|
||||
maxX < mouseSelectionLeft + mouseSelectionWidth &&
|
||||
minY > (mouseSelectionTop - mouseSelectionHeight) &&
|
||||
maxY < (mouseSelectionTop - mouseSelectionHeight) + mouseSelectionHeight
|
||||
}
|
||||
else if(quadrant === 3) {
|
||||
isInclude = minX > (mouseSelectionLeft - mouseSelectionWidth) &&
|
||||
maxX < (mouseSelectionLeft - mouseSelectionWidth) + mouseSelectionWidth &&
|
||||
minY > mouseSelectionTop &&
|
||||
maxY < mouseSelectionTop + mouseSelectionHeight
|
||||
}
|
||||
|
||||
// 被锁定的元素除外
|
||||
if(isInclude && !element.isLock) inRangeElementList.push(element)
|
||||
}
|
||||
|
||||
// 对于组合元素成员,必须所有成员都在选择范围中才算被选中
|
||||
inRangeElementList = inRangeElementList.filter(inRangeElement => {
|
||||
if(inRangeElement.groupId) {
|
||||
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
|
||||
const groupElementList = elementList.value.filter(element => element.groupId === inRangeElement.groupId)
|
||||
return groupElementList.every(groupElement => inRangeElementIdList.includes(groupElement.elId))
|
||||
}
|
||||
return true
|
||||
})
|
||||
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
|
||||
if(inRangeElementIdList.length) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
|
||||
|
||||
mouseSelectionState.isShow = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mouseSelectionState,
|
||||
updateMouseSelection,
|
||||
}
|
||||
}
|
313
src/views/Editor/Canvas/hooks/useMoveElement.ts
Normal file
313
src/views/Editor/Canvas/hooks/useMoveElement.ts
Normal file
@ -0,0 +1,313 @@
|
||||
import { Ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { ElementTypes, PPTElement } from '@/types/slides'
|
||||
import { AlignmentLineProps } from '../types/index'
|
||||
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||
import { getRectRotatedRange } from '../utils/elementRange'
|
||||
import { AlignLine, uniqAlignLines } from '../utils/alignLines'
|
||||
|
||||
export default (
|
||||
elementList: Ref<PPTElement[]>,
|
||||
activeElementIdList: Ref<string[]>,
|
||||
activeGroupElementId: Ref<string>,
|
||||
canvasScale: Ref<number>,
|
||||
alignmentLines: Ref<AlignmentLineProps[]>,
|
||||
) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
const moveElement = (e: MouseEvent, element: PPTElement) => {
|
||||
if(!activeElementIdList.value.includes(element.elId)) return
|
||||
let isMouseDown = true
|
||||
|
||||
// 可视范围宽高,用于边缘对齐吸附
|
||||
const edgeWidth = VIEWPORT_SIZE
|
||||
const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
|
||||
|
||||
const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
|
||||
const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.elId))
|
||||
|
||||
const sorptionRange = 3
|
||||
const elOriginLeft = element.left
|
||||
const elOriginTop = element.top
|
||||
const elOriginWidth = element.width
|
||||
const elOriginHeight = ('height' in element && element.height) ? element.height : 0
|
||||
const elOriginRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
|
||||
let isMisoperation: boolean | null = null
|
||||
|
||||
const isActiveGroupElement = element.elId === activeGroupElementId.value
|
||||
|
||||
// 收集对齐参考线
|
||||
// 包括页面内出被操作元素以外的所有元素在页面内水平和垂直方向的范围和中心位置、页面边界和水平和垂直的中心位置
|
||||
let horizontalLines: AlignLine[] = []
|
||||
let verticalLines: AlignLine[] = []
|
||||
|
||||
// 元素在页面内水平和垂直方向的范围和中心位置(需要特殊计算线条和被旋转的元素)
|
||||
for(const el of elementList.value) {
|
||||
if(el.type === ElementTypes.LINE) continue
|
||||
if(isActiveGroupElement && el.elId === element.elId) continue
|
||||
if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) continue
|
||||
|
||||
let left, top, width, height
|
||||
if('rotate' in el && el.rotate) {
|
||||
const { xRange, yRange } = getRectRotatedRange({
|
||||
left: el.left,
|
||||
top: el.top,
|
||||
width: el.width,
|
||||
height: el.height,
|
||||
rotate: el.rotate,
|
||||
})
|
||||
left = xRange[0]
|
||||
top = yRange[0]
|
||||
width = xRange[1] - xRange[0]
|
||||
height = yRange[1] - yRange[0]
|
||||
}
|
||||
else {
|
||||
left = el.left
|
||||
top = el.top
|
||||
width = el.width
|
||||
height = el.height
|
||||
}
|
||||
|
||||
const right = left + width
|
||||
const bottom = top + height
|
||||
const centerX = top + height / 2
|
||||
const centerY = left + width / 2
|
||||
|
||||
const topLine: AlignLine = { value: top, range: [left, right] }
|
||||
const bottomLine: AlignLine = { value: bottom, range: [left, right] }
|
||||
const horizontalCenterLine: AlignLine = { value: centerX, range: [left, right] }
|
||||
const leftLine: AlignLine = { value: left, range: [top, bottom] }
|
||||
const rightLine: AlignLine = { value: right, range: [top, bottom] }
|
||||
const verticalCenterLine: AlignLine = { value: centerY, range: [top, bottom] }
|
||||
|
||||
horizontalLines.push(topLine, bottomLine, horizontalCenterLine)
|
||||
verticalLines.push(leftLine, rightLine, verticalCenterLine)
|
||||
}
|
||||
|
||||
// 页面边界、水平和垂直的中心位置
|
||||
const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }
|
||||
const edgeBottomLine: AlignLine = { value: edgeHeight, range: [0, edgeWidth] }
|
||||
const edgeHorizontalCenterLine: AlignLine = { value: edgeHeight / 2, range: [0, edgeWidth] }
|
||||
const edgeLeftLine: AlignLine = { value: 0, range: [0, edgeHeight] }
|
||||
const edgeRightLine: AlignLine = { value: edgeWidth, range: [0, edgeHeight] }
|
||||
const edgeVerticalCenterLine: AlignLine = { value: edgeWidth / 2, range: [0, edgeHeight] }
|
||||
|
||||
horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine)
|
||||
verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine)
|
||||
|
||||
// 参考线去重
|
||||
horizontalLines = uniqAlignLines(horizontalLines)
|
||||
verticalLines = uniqAlignLines(verticalLines)
|
||||
|
||||
document.onmousemove = e => {
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
// 对于鼠标第一次滑动距离过小的操作判定为误操作
|
||||
// 这里仅在误操作标记未被赋值(null,第一次触发移动),以及被标记为误操作时(true,当前处于误操作范围,但可能会脱离该范围转变成正常操作),才会去计算
|
||||
// 已经被标记为非误操作时(false),不需要再次计算(因为不可能从非误操作转变成误操作)
|
||||
if(isMisoperation !== false) {
|
||||
isMisoperation = Math.abs(startPageX - currentPageX) < sorptionRange &&
|
||||
Math.abs(startPageY - currentPageY) < sorptionRange
|
||||
}
|
||||
if( !isMouseDown || isMisoperation ) return
|
||||
|
||||
// 鼠标按下后移动的距离
|
||||
const moveX = (currentPageX - startPageX) / canvasScale.value
|
||||
const moveY = (currentPageY - startPageY) / canvasScale.value
|
||||
|
||||
// 被操作元素需要移动到的位置
|
||||
let targetLeft = elOriginLeft + moveX
|
||||
let targetTop = elOriginTop + moveY
|
||||
|
||||
// 计算被操作元素在页面中的范围(用于吸附对齐)
|
||||
// 需要区分计算:多选状态、线条、被旋转的元素
|
||||
// 注意这里需要用元素的原始信息结合移动信息来计算
|
||||
let targetMinX: number, targetMaxX: number, targetMinY: number, targetMaxY: number
|
||||
|
||||
if(activeElementIdList.value.length === 1 || isActiveGroupElement) {
|
||||
if(elOriginRotate) {
|
||||
const { xRange, yRange } = getRectRotatedRange({
|
||||
left: targetLeft,
|
||||
top: targetTop,
|
||||
width: elOriginWidth,
|
||||
height: elOriginHeight,
|
||||
rotate: elOriginRotate,
|
||||
})
|
||||
targetMinX = xRange[0]
|
||||
targetMaxX = xRange[1]
|
||||
targetMinY = yRange[0]
|
||||
targetMaxY = yRange[1]
|
||||
}
|
||||
else if(element.type === 'line') {
|
||||
targetMinX = targetLeft
|
||||
targetMaxX = targetLeft + Math.max(element.start[0], element.end[0])
|
||||
targetMinY = targetTop
|
||||
targetMaxY = targetTop + Math.max(element.start[1], element.end[1])
|
||||
}
|
||||
else {
|
||||
targetMinX = targetLeft
|
||||
targetMaxX = targetLeft + elOriginWidth
|
||||
targetMinY = targetTop
|
||||
targetMaxY = targetTop + elOriginHeight
|
||||
}
|
||||
}
|
||||
else {
|
||||
const leftValues = []
|
||||
const topValues = []
|
||||
const rightValues = []
|
||||
const bottomValues = []
|
||||
|
||||
for(let i = 0; i < originActiveElementList.length; i++) {
|
||||
const element = originActiveElementList[i]
|
||||
const left = element.left + moveX
|
||||
const top = element.top + moveY
|
||||
const width = element.width
|
||||
const height = ('height' in element && element.height) ? element.height : 0
|
||||
const rotate = ('rotate' in element && element.rotate) ? element.rotate : 0
|
||||
|
||||
if('rotate' in element && element.rotate) {
|
||||
const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate })
|
||||
leftValues.push(xRange[0])
|
||||
topValues.push(yRange[0])
|
||||
rightValues.push(xRange[1])
|
||||
bottomValues.push(yRange[1])
|
||||
}
|
||||
else if(element.type === 'line') {
|
||||
leftValues.push(left)
|
||||
topValues.push(top)
|
||||
rightValues.push(left + Math.max(element.start[0], element.end[0]))
|
||||
bottomValues.push(top + Math.max(element.start[1], element.end[1]))
|
||||
}
|
||||
else {
|
||||
leftValues.push(left)
|
||||
topValues.push(top)
|
||||
rightValues.push(left + width)
|
||||
bottomValues.push(top + height)
|
||||
}
|
||||
}
|
||||
|
||||
targetMinX = Math.min(...leftValues)
|
||||
targetMaxX = Math.max(...rightValues)
|
||||
targetMinY = Math.min(...topValues)
|
||||
targetMaxY = Math.max(...bottomValues)
|
||||
}
|
||||
|
||||
const targetCenterX = targetMinX + (targetMaxX - targetMinX) / 2
|
||||
const targetCenterY = targetMinY + (targetMaxY - targetMinY) / 2
|
||||
|
||||
// 根据收集到的参考线,分别执行垂直和水平两个方向的对齐吸附
|
||||
const _alignmentLines: AlignmentLineProps[] = []
|
||||
let isVerticalAdsorbed = false
|
||||
let isHorizontalAdsorbed = false
|
||||
for(let i = 0; i < horizontalLines.length; i++) {
|
||||
const { value, range } = horizontalLines[i]
|
||||
const min = Math.min(...range, targetMinX, targetMaxX)
|
||||
const max = Math.max(...range, targetMinX, targetMaxX)
|
||||
|
||||
if(Math.abs(targetMinY - value) < sorptionRange) {
|
||||
if(!isHorizontalAdsorbed) {
|
||||
targetTop = targetTop - (targetMinY - value)
|
||||
isHorizontalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
||||
}
|
||||
if(Math.abs(targetMaxY - value) < sorptionRange) {
|
||||
if(!isHorizontalAdsorbed) {
|
||||
targetTop = targetTop - (targetMaxY - value)
|
||||
isHorizontalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
||||
}
|
||||
if(Math.abs(targetCenterY - value) < sorptionRange) {
|
||||
if(!isHorizontalAdsorbed) {
|
||||
targetTop = targetTop - (targetCenterY - value)
|
||||
isHorizontalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
||||
}
|
||||
}
|
||||
for(let i = 0; i < verticalLines.length; i++) {
|
||||
const { value, range } = verticalLines[i]
|
||||
const min = Math.min(...range, targetMinY, targetMaxY)
|
||||
const max = Math.max(...range, targetMinY, targetMaxY)
|
||||
|
||||
if(Math.abs(targetMinX - value) < sorptionRange) {
|
||||
if(!isVerticalAdsorbed) {
|
||||
targetLeft = targetLeft - (targetMinX - value)
|
||||
isVerticalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
|
||||
}
|
||||
if(Math.abs(targetMaxX - value) < sorptionRange) {
|
||||
if(!isVerticalAdsorbed) {
|
||||
targetLeft = targetLeft - (targetMaxX - value)
|
||||
isVerticalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
|
||||
}
|
||||
if(Math.abs(targetCenterX - value) < sorptionRange) {
|
||||
if(!isVerticalAdsorbed) {
|
||||
targetLeft = targetLeft - (targetCenterX - value)
|
||||
isVerticalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
|
||||
}
|
||||
}
|
||||
alignmentLines.value = _alignmentLines
|
||||
|
||||
// 非多选,或者当前操作的元素时激活的组合元素
|
||||
if(activeElementIdList.value.length === 1 || isActiveGroupElement) {
|
||||
elementList.value = elementList.value.map(el => {
|
||||
return el.elId === element.elId ? { ...el, left: targetLeft, top: targetTop } : el
|
||||
})
|
||||
}
|
||||
|
||||
// 修改元素位置,如果需要修改位置的元素不是被操作的元素(例如多选下的操作)
|
||||
// 那么其他非操作元素要移动的位置通过操作元素的移动偏移量计算
|
||||
else {
|
||||
const handleElement = elementList.value.find(el => el.elId === element.elId)
|
||||
if(!handleElement) return
|
||||
|
||||
elementList.value = elementList.value.map(el => {
|
||||
const newEl = el
|
||||
activeElementIdList.value.includes(el.elId) ? { ...el, left: targetLeft, top: targetTop } : el
|
||||
if(activeElementIdList.value.includes(el.elId)) {
|
||||
if(el.elId === element.elId) {
|
||||
newEl.left = targetLeft
|
||||
newEl.top = targetTop
|
||||
}
|
||||
else {
|
||||
newEl.left = newEl.left + (targetLeft - handleElement.left)
|
||||
newEl.top = newEl.top + (targetTop - handleElement.top)
|
||||
}
|
||||
}
|
||||
return newEl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
document.onmouseup = e => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
alignmentLines.value = []
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
// 对比初始位置,没有实际的位移不更新数据
|
||||
if(startPageX === currentPageX && startPageY === currentPageY) return
|
||||
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
moveElement,
|
||||
}
|
||||
}
|
181
src/views/Editor/Canvas/hooks/useOrderElement.ts
Normal file
181
src/views/Editor/Canvas/hooks/useOrderElement.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { useStore } from 'vuex'
|
||||
import { Ref } from 'vue'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
import { ElementOrderCommand, ElementOrderCommands } from '@/types/edit'
|
||||
|
||||
export default (elementList: Ref<PPTElement[]>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
// 获取组合元素层级范围(组合成员中的最大层级和最小层级)
|
||||
const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
|
||||
const minIndex = elementList.findIndex(_element => _element.elId === combineElementList[0].elId)
|
||||
const maxIndex = elementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
|
||||
return { minIndex, maxIndex }
|
||||
}
|
||||
|
||||
// 上移一层,返回移动后新的元素列表(下移一层逻辑类似)
|
||||
const moveUpElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
// 被操作的元素是组合元素成员
|
||||
if(element.groupId) {
|
||||
|
||||
// 获取该组合元素全部成员,以及组合元素层级范围
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(maxIndex === elementList.length - 1) return null
|
||||
|
||||
// 该组合元素上一层的元素,以下简称为【元素next】
|
||||
const nextElement = copyOfElementList[maxIndex + 1]
|
||||
|
||||
// 从元素列表中移除该组合元素全部成员
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
|
||||
// 如果【元素next】也是组合元素成员(另一个组合,不是上面被移除的那一组,以下简称为【组合next】)
|
||||
// 需要获取【组合next】全部成员的长度,将上面移除的组合元素全部成员添加到【组合next】全部成员的上方
|
||||
if(nextElement.groupId) {
|
||||
const nextCombineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
|
||||
copyOfElementList.splice(minIndex + nextCombineElementList.length, 0, ...movedElementList)
|
||||
}
|
||||
|
||||
// 如果【元素next】是单独的元素(非组合成员),将上面移除的组合元素全部成员添加到【元素next】上方
|
||||
else copyOfElementList.splice(minIndex + 1, 0, ...movedElementList)
|
||||
}
|
||||
|
||||
// 被操作的元素是单独的元素(非组合成员)
|
||||
else {
|
||||
|
||||
// 元素在元素列表中的层级
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(elementIndex === elementList.length - 1) return null
|
||||
|
||||
// 上一层的元素,以下简称为【元素next】
|
||||
const nextElement = copyOfElementList[elementIndex + 1]
|
||||
|
||||
// 从元素列表中移除被操作的元素
|
||||
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
|
||||
|
||||
// 如果【元素next】是组合元素成员
|
||||
// 需要获取该组合全部成员的长度,将上面移除的元素添加到该组合全部成员的上方
|
||||
if(nextElement.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
|
||||
copyOfElementList.splice(elementIndex + combineElementList.length, 0, movedElement)
|
||||
}
|
||||
|
||||
// 如果【元素next】是单独的元素(非组合成员),将上面移除的元素添加到【元素next】上方
|
||||
else copyOfElementList.splice(elementIndex + 1, 0, movedElement)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
// 下移一层
|
||||
const moveDownElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
if(element.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
if(minIndex === 0) return null
|
||||
const prevElement = copyOfElementList[minIndex - 1]
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
if(prevElement.groupId) {
|
||||
const prevCombineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
|
||||
copyOfElementList.splice(minIndex - prevCombineElementList.length, 0, ...movedElementList)
|
||||
}
|
||||
else copyOfElementList.splice(minIndex - 1, 0, ...movedElementList)
|
||||
}
|
||||
|
||||
else {
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
if(elementIndex === 0) return null
|
||||
const prevElement = copyOfElementList[elementIndex - 1]
|
||||
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
|
||||
if(prevElement.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
|
||||
copyOfElementList.splice(elementIndex - combineElementList.length, 0, movedElement)
|
||||
}
|
||||
else copyOfElementList.splice(elementIndex - 1, 0, movedElement)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
// 置顶层,返回移动后新的元素列表(置底层逻辑类似)
|
||||
const moveTopElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
// 被操作的元素是组合元素成员
|
||||
if(element.groupId) {
|
||||
|
||||
// 获取该组合元素全部成员,以及组合元素层级范围
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(maxIndex === elementList.length - 1) return null
|
||||
|
||||
// 从元素列表中移除该组合元素全部成员,然后添加到元素列表最上方
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
copyOfElementList.push(...movedElementList)
|
||||
}
|
||||
|
||||
// 被操作的元素是单独的元素(非组合成员)
|
||||
else {
|
||||
|
||||
// 元素在元素列表中的层级
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(elementIndex === elementList.length - 1) return null
|
||||
|
||||
// 从元素列表中移除该元素,然后添加到元素列表最上方
|
||||
copyOfElementList.splice(elementIndex, 1)
|
||||
copyOfElementList.push(element)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
// 置底层
|
||||
const moveBottomElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
if(element.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
if(minIndex === 0) return null
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
copyOfElementList.unshift(...movedElementList)
|
||||
}
|
||||
|
||||
else {
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
if(elementIndex === 0) return null
|
||||
copyOfElementList.splice(elementIndex, 1)
|
||||
copyOfElementList.unshift(element)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
const orderElement = (element: PPTElement, command: ElementOrderCommand) => {
|
||||
let newElementList = null
|
||||
|
||||
if(command === ElementOrderCommands.UP) newElementList = moveUpElement(elementList.value, element)
|
||||
else if(command === ElementOrderCommands.DOWN) newElementList = moveDownElement(elementList.value, element)
|
||||
else if(command === ElementOrderCommands.TOP) newElementList = moveTopElement(elementList.value, element)
|
||||
else if(command === ElementOrderCommands.BOTTOM) newElementList = moveBottomElement(elementList.value, element)
|
||||
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
|
||||
}
|
||||
|
||||
return {
|
||||
orderElement,
|
||||
}
|
||||
}
|
67
src/views/Editor/Canvas/hooks/useRotateElement.ts
Normal file
67
src/views/Editor/Canvas/hooks/useRotateElement.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement } from '@/types/slides'
|
||||
import { getAngleFromCoordinate } from '../utils/elementRotate'
|
||||
|
||||
export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | null>, canvasScale: Ref<number>) => {
|
||||
const store = useStore<State>()
|
||||
|
||||
const rotateElement = (element: PPTTextElement | PPTImageElement | PPTShapeElement) => {
|
||||
let isMouseDown = true
|
||||
let angle = 0
|
||||
const elOriginRotate = element.rotate || 0
|
||||
|
||||
// 计算元素中心(旋转的中心,坐标原点)
|
||||
const elLeft = element.left
|
||||
const elTop = element.top
|
||||
const elWidth = element.width
|
||||
const elHeight = element.height
|
||||
const centerX = elLeft + elWidth / 2
|
||||
const centerY = elTop + elHeight / 2
|
||||
|
||||
if(!viewportRef.value) return
|
||||
const viewportRect = viewportRef.value.getBoundingClientRect()
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
// 计算鼠标基于旋转中心的坐标
|
||||
const mouseX = (e.pageX - viewportRect.left) / canvasScale.value
|
||||
const mouseY = (e.pageY - viewportRect.top) / canvasScale.value
|
||||
const x = mouseX - centerX
|
||||
const y = centerY - mouseY
|
||||
|
||||
angle = getAngleFromCoordinate(x, y)
|
||||
|
||||
// 45°的倍数位置有吸附效果
|
||||
const sorptionRange = 5
|
||||
if( Math.abs(angle) <= sorptionRange ) angle = 0
|
||||
else if( angle > 0 && Math.abs(angle - 45) <= sorptionRange ) angle -= (angle - 45)
|
||||
else if( angle < 0 && Math.abs(angle + 45) <= sorptionRange ) angle -= (angle + 45)
|
||||
else if( angle > 0 && Math.abs(angle - 90) <= sorptionRange ) angle -= (angle - 90)
|
||||
else if( angle < 0 && Math.abs(angle + 90) <= sorptionRange ) angle -= (angle + 90)
|
||||
else if( angle > 0 && Math.abs(angle - 135) <= sorptionRange ) angle -= (angle - 135)
|
||||
else if( angle < 0 && Math.abs(angle + 135) <= sorptionRange ) angle -= (angle + 135)
|
||||
else if( angle > 0 && Math.abs(angle - 180) <= sorptionRange ) angle -= (angle - 180)
|
||||
else if( angle < 0 && Math.abs(angle + 180) <= sorptionRange ) angle -= (angle + 180)
|
||||
|
||||
// 修改元素角度
|
||||
elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...el, rotate: angle } : el)
|
||||
}
|
||||
|
||||
document.onmouseup = () => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
if(elOriginRotate === angle) return
|
||||
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rotateElement,
|
||||
}
|
||||
}
|
423
src/views/Editor/Canvas/hooks/useScaleElement.ts
Normal file
423
src/views/Editor/Canvas/hooks/useScaleElement.ts
Normal file
@ -0,0 +1,423 @@
|
||||
import { computed, Ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { ElementTypes, PPTElement, PPTLineElement } from '@/types/slides'
|
||||
import { OPERATE_KEYS, ElementScaleHandler } from '@/types/edit'
|
||||
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||
import { getRotateElementPoints, getOppositePoint } from '../utils/elementRotate'
|
||||
import { AlignLine, uniqAlignLines } from '../utils/alignLines'
|
||||
import { AlignmentLineProps, MultiSelectRange } from '../types/index'
|
||||
|
||||
export default (
|
||||
elementList: Ref<PPTElement[]>,
|
||||
canvasScale: Ref<number>,
|
||||
activeGroupElementId: Ref<string>,
|
||||
activeElementIdList: Ref<string[]>,
|
||||
alignmentLines: Ref<AlignmentLineProps[]>,
|
||||
) => {
|
||||
const store = useStore<State>()
|
||||
const ctrlOrShiftKeyActive = computed(() => store.getters.ctrlOrShiftKeyActive)
|
||||
|
||||
const scaleElement = (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: ElementScaleHandler) => {
|
||||
let isMouseDown = true
|
||||
|
||||
const elOriginLeft = element.left
|
||||
const elOriginTop = element.top
|
||||
const elOriginWidth = element.width
|
||||
const elOriginHeight = element.height
|
||||
|
||||
const isLockRatio = ctrlOrShiftKeyActive.value || ('lockRatio' in element && element.lockRatio)
|
||||
const lockRatio = elOriginWidth / elOriginHeight
|
||||
|
||||
const elRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
|
||||
const rotateRadian = Math.PI * elRotate / 180
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
|
||||
const minSize = 15
|
||||
const getSizeWithinRange = (size: number) => size < minSize ? minSize : size
|
||||
|
||||
let points: ReturnType<typeof getRotateElementPoints>
|
||||
let baseLeft = 0
|
||||
let baseTop = 0
|
||||
let horizontalLines: AlignLine[] = []
|
||||
let verticalLines: AlignLine[] = []
|
||||
|
||||
if('rotate' in element && element.rotate) {
|
||||
// 元素旋转后的各点坐标以及对角坐标
|
||||
const { left, top, width, height } = element
|
||||
points = getRotateElementPoints({ left, top, width, height }, elRotate)
|
||||
const oppositePoint = getOppositePoint(command, points)
|
||||
|
||||
// 基点坐标(以操作点相对的点为基点,例如拖动右下角,实际上是保持左上角不变的前提下修改其他信息)
|
||||
baseLeft = oppositePoint.left
|
||||
baseTop = oppositePoint.top
|
||||
}
|
||||
else {
|
||||
const edgeWidth = VIEWPORT_SIZE
|
||||
const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
|
||||
const isActiveGroupElement = element.elId === activeGroupElementId.value
|
||||
|
||||
for(const el of elementList.value) {
|
||||
if('rotate' in el && el.rotate) continue
|
||||
if(el.type === ElementTypes.LINE) continue
|
||||
if(isActiveGroupElement && el.elId === element.elId) continue
|
||||
if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) continue
|
||||
|
||||
const left = el.left
|
||||
const top = el.top
|
||||
const width = el.width
|
||||
const height = el.height
|
||||
const right = left + width
|
||||
const bottom = top + height
|
||||
|
||||
const topLine: AlignLine = { value: top, range: [left, right] }
|
||||
const bottomLine: AlignLine = { value: bottom, range: [left, right] }
|
||||
const leftLine: AlignLine = { value: left, range: [top, bottom] }
|
||||
const rightLine: AlignLine = { value: right, range: [top, bottom] }
|
||||
|
||||
horizontalLines.push(topLine, bottomLine)
|
||||
verticalLines.push(leftLine, rightLine)
|
||||
}
|
||||
|
||||
// 页面边界、水平和垂直的中心位置
|
||||
const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }
|
||||
const edgeBottomLine: AlignLine = { value: edgeHeight, range: [0, edgeWidth] }
|
||||
const edgeHorizontalCenterLine: AlignLine = { value: edgeHeight / 2, range: [0, edgeWidth] }
|
||||
const edgeLeftLine: AlignLine = { value: 0, range: [0, edgeHeight] }
|
||||
const edgeRightLine: AlignLine = { value: edgeWidth, range: [0, edgeHeight] }
|
||||
const edgeVerticalCenterLine: AlignLine = { value: edgeWidth / 2, range: [0, edgeHeight] }
|
||||
|
||||
horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine)
|
||||
verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine)
|
||||
|
||||
horizontalLines = uniqAlignLines(horizontalLines)
|
||||
verticalLines = uniqAlignLines(verticalLines)
|
||||
}
|
||||
|
||||
// 对齐吸附方法
|
||||
const alignedAdsorption = (currentX: number | null, currentY: number | null) => {
|
||||
const sorptionRange = 3
|
||||
|
||||
const _alignmentLines: AlignmentLineProps[] = []
|
||||
let isVerticalAdsorbed = false
|
||||
let isHorizontalAdsorbed = false
|
||||
const correctionVal = { offsetX: 0, offsetY: 0 }
|
||||
|
||||
if(currentY || currentY === 0) {
|
||||
for(let i = 0; i < horizontalLines.length; i++) {
|
||||
const { value, range } = horizontalLines[i]
|
||||
const min = Math.min(...range, currentX || 0)
|
||||
const max = Math.max(...range, currentX || 0)
|
||||
|
||||
if(Math.abs(currentY - value) < sorptionRange) {
|
||||
if(!isHorizontalAdsorbed) {
|
||||
correctionVal.offsetY = currentY - value
|
||||
isHorizontalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
|
||||
}
|
||||
}
|
||||
}
|
||||
if(currentX || currentX === 0) {
|
||||
for(let i = 0; i < verticalLines.length; i++) {
|
||||
const { value, range } = verticalLines[i]
|
||||
const min = Math.min(...range, (currentY || 0))
|
||||
const max = Math.max(...range, (currentY || 0))
|
||||
|
||||
if(Math.abs(currentX - value) < sorptionRange) {
|
||||
if(!isVerticalAdsorbed) {
|
||||
correctionVal.offsetX = currentX - value
|
||||
isVerticalAdsorbed = true
|
||||
}
|
||||
_alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40 })
|
||||
}
|
||||
}
|
||||
}
|
||||
alignmentLines.value = _alignmentLines
|
||||
return correctionVal
|
||||
}
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
const x = currentPageX - startPageX
|
||||
const y = currentPageY - startPageY
|
||||
|
||||
let width = elOriginWidth
|
||||
let height = elOriginHeight
|
||||
let left = elOriginLeft
|
||||
let top = elOriginTop
|
||||
|
||||
// 元素被旋转的情况下
|
||||
if(elRotate) {
|
||||
// 根据元素旋转的角度,修正鼠标按下后移动的距离(因为拖动的方向发生了改变)
|
||||
const revisedX = (Math.cos(rotateRadian) * x + Math.sin(rotateRadian) * y) / canvasScale.value
|
||||
let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale.value
|
||||
|
||||
// 锁定宽高比例
|
||||
if(isLockRatio) {
|
||||
if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) revisedY = revisedX / lockRatio
|
||||
if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) revisedY = -revisedX / lockRatio
|
||||
}
|
||||
|
||||
// 根据不同的操作点分别计算元素缩放后的大小和位置
|
||||
// 这里计算的位置是错误的,因为旋转后缩放实际上也改变了元素的位置,需要在后面进行矫正
|
||||
// 这里计算的大小是正确的,因为上面修正鼠标按下后移动的距离时其实已经进行过了矫正
|
||||
if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
|
||||
width = getSizeWithinRange(elOriginWidth + revisedX)
|
||||
height = getSizeWithinRange(elOriginHeight + revisedY)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
|
||||
width = getSizeWithinRange(elOriginWidth - revisedX)
|
||||
height = getSizeWithinRange(elOriginHeight + revisedY)
|
||||
left = elOriginLeft - (width - elOriginWidth)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT_TOP) {
|
||||
width = getSizeWithinRange(elOriginWidth - revisedX)
|
||||
height = getSizeWithinRange(elOriginHeight - revisedY)
|
||||
left = elOriginLeft - (width - elOriginWidth)
|
||||
top = elOriginTop - (height - elOriginHeight)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.RIGHT_TOP) {
|
||||
width = getSizeWithinRange(elOriginWidth + revisedX)
|
||||
height = getSizeWithinRange(elOriginHeight - revisedY)
|
||||
top = elOriginTop - (height - elOriginHeight)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.TOP) {
|
||||
height = getSizeWithinRange(elOriginHeight - revisedY)
|
||||
top = elOriginTop - (height - elOriginHeight)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.BOTTOM) {
|
||||
height = getSizeWithinRange(elOriginHeight + revisedY)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT) {
|
||||
width = getSizeWithinRange(elOriginWidth - revisedX)
|
||||
left = elOriginLeft - (width - elOriginWidth)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.RIGHT) {
|
||||
width = getSizeWithinRange(elOriginWidth + revisedX)
|
||||
}
|
||||
|
||||
// 获取当前元素基点坐标,与初始状态的基点坐标进行对比并矫正差值
|
||||
const currentPoints = getRotateElementPoints({ width, height, left, top }, elRotate)
|
||||
const currentOppositePoint = getOppositePoint(command, currentPoints)
|
||||
const currentBaseLeft = currentOppositePoint.left
|
||||
const currentBaseTop = currentOppositePoint.top
|
||||
|
||||
const offsetX = currentBaseLeft - baseLeft
|
||||
const offsetY = currentBaseTop - baseTop
|
||||
|
||||
left = left - offsetX
|
||||
top = top - offsetY
|
||||
}
|
||||
|
||||
// 元素未被旋转的情况下,根据所操纵点的位置添加对齐吸附
|
||||
else {
|
||||
let moveX = x / canvasScale.value
|
||||
let moveY = y / canvasScale.value
|
||||
|
||||
if(isLockRatio) {
|
||||
if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) moveY = moveX / lockRatio
|
||||
if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) moveY = -moveX / lockRatio
|
||||
}
|
||||
|
||||
if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
|
||||
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + elOriginHeight + moveY)
|
||||
moveX = moveX - offsetX
|
||||
moveY = moveY - offsetY
|
||||
if(isLockRatio) {
|
||||
if(offsetY) moveX = moveY * lockRatio
|
||||
else moveY = moveX / lockRatio
|
||||
}
|
||||
width = getSizeWithinRange(elOriginWidth + moveX)
|
||||
height = getSizeWithinRange(elOriginHeight + moveY)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
|
||||
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + elOriginHeight + moveY)
|
||||
moveX = moveX - offsetX
|
||||
moveY = moveY - offsetY
|
||||
if(isLockRatio) {
|
||||
if(offsetY) moveX = -moveY * lockRatio
|
||||
else moveY = -moveX / lockRatio
|
||||
}
|
||||
width = getSizeWithinRange(elOriginWidth - moveX)
|
||||
height = getSizeWithinRange(elOriginHeight + moveY)
|
||||
left = elOriginLeft - (width - elOriginWidth)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT_TOP) {
|
||||
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + moveY)
|
||||
moveX = moveX - offsetX
|
||||
moveY = moveY - offsetY
|
||||
if(isLockRatio) {
|
||||
if(offsetY) moveX = moveY * lockRatio
|
||||
else moveY = moveX / lockRatio
|
||||
}
|
||||
width = getSizeWithinRange(elOriginWidth - moveX)
|
||||
height = getSizeWithinRange(elOriginHeight - moveY)
|
||||
left = elOriginLeft - (width - elOriginWidth)
|
||||
top = elOriginTop - (height - elOriginHeight)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.RIGHT_TOP) {
|
||||
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + moveY)
|
||||
moveX = moveX - offsetX
|
||||
moveY = moveY - offsetY
|
||||
if(isLockRatio) {
|
||||
if(offsetY) moveX = -moveY * lockRatio
|
||||
else moveY = -moveX / lockRatio
|
||||
}
|
||||
width = getSizeWithinRange(elOriginWidth + moveX)
|
||||
height = getSizeWithinRange(elOriginHeight - moveY)
|
||||
top = elOriginTop - (height - elOriginHeight)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT) {
|
||||
const { offsetX } = alignedAdsorption(elOriginLeft + moveX, null)
|
||||
moveX = moveX - offsetX
|
||||
width = getSizeWithinRange(elOriginWidth - moveX)
|
||||
left = elOriginLeft - (width - elOriginWidth)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.RIGHT) {
|
||||
const { offsetX } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, null)
|
||||
moveX = moveX - offsetX
|
||||
width = getSizeWithinRange(elOriginWidth + moveX)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.TOP) {
|
||||
const { offsetY } = alignedAdsorption(null, elOriginTop + moveY)
|
||||
moveY = moveY - offsetY
|
||||
height = getSizeWithinRange(elOriginHeight - moveY)
|
||||
top = elOriginTop - (height - elOriginHeight)
|
||||
}
|
||||
else if(command === OPERATE_KEYS.BOTTOM) {
|
||||
const { offsetY } = alignedAdsorption(null, elOriginTop + elOriginHeight + moveY)
|
||||
moveY = moveY - offsetY
|
||||
height = getSizeWithinRange(elOriginHeight + moveY)
|
||||
}
|
||||
}
|
||||
|
||||
elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...el, left, top, width, height } : el)
|
||||
}
|
||||
|
||||
document.onmouseup = e => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
alignmentLines.value = []
|
||||
|
||||
if(startPageX === e.pageX && startPageY === e.pageY) return
|
||||
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
|
||||
}
|
||||
}
|
||||
|
||||
const scaleMultiElement = (e: MouseEvent, range: MultiSelectRange, command: ElementScaleHandler) => {
|
||||
let isMouseDown = true
|
||||
|
||||
const { minX, maxX, minY, maxY } = range
|
||||
const operateWidth = maxX - minX
|
||||
const operateHeight = maxY - minY
|
||||
const lockRatio = operateWidth / operateHeight
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
|
||||
const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
// 鼠标按下后移动的距离
|
||||
const x = (currentPageX - startPageX) / canvasScale.value
|
||||
let y = (currentPageY - startPageY) / canvasScale.value
|
||||
|
||||
// 锁定宽高比例
|
||||
if(ctrlOrShiftKeyActive.value) {
|
||||
if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) y = x / lockRatio
|
||||
if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) y = -x / lockRatio
|
||||
}
|
||||
|
||||
// 获取鼠标缩放时当前所有激活元素的范围
|
||||
let currentMinX = minX
|
||||
let currentMaxX = maxX
|
||||
let currentMinY = minY
|
||||
let currentMaxY = maxY
|
||||
|
||||
if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
|
||||
currentMaxX = maxX + x
|
||||
currentMaxY = maxY + y
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
|
||||
currentMinX = minX + x
|
||||
currentMaxY = maxY + y
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT_TOP) {
|
||||
currentMinX = minX + x
|
||||
currentMinY = minY + y
|
||||
}
|
||||
else if(command === OPERATE_KEYS.RIGHT_TOP) {
|
||||
currentMaxX = maxX + x
|
||||
currentMinY = minY + y
|
||||
}
|
||||
else if(command === OPERATE_KEYS.TOP) {
|
||||
currentMinY = minY + y
|
||||
}
|
||||
else if(command === OPERATE_KEYS.BOTTOM) {
|
||||
currentMaxY = maxY + y
|
||||
}
|
||||
else if(command === OPERATE_KEYS.LEFT) {
|
||||
currentMinX = minX + x
|
||||
}
|
||||
else if(command === OPERATE_KEYS.RIGHT) {
|
||||
currentMaxX = maxX + x
|
||||
}
|
||||
|
||||
// 多选下所有元素整体宽高
|
||||
const currentOppositeWidth = currentMaxX - currentMinX
|
||||
const currentOppositeHeight = currentMaxY - currentMinY
|
||||
|
||||
// 所有元素的整体宽高与被操作元素宽高的比例
|
||||
let widthScale = currentOppositeWidth / operateWidth
|
||||
let heightScale = currentOppositeHeight / operateHeight
|
||||
|
||||
if(widthScale <= 0) widthScale = 0
|
||||
if(heightScale <= 0) heightScale = 0
|
||||
|
||||
// 根据上面计算的比例,修改所有被激活元素的位置大小
|
||||
// 宽高通过乘以对应的比例得到,位置通过将被操作元素在所有元素整体中的相对位置乘以对应比例获得
|
||||
elementList.value = elementList.value.map(el => {
|
||||
const newEl = el
|
||||
if((newEl.type === ElementTypes.IMAGE || newEl.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(newEl.elId)) {
|
||||
const originElement = originElementList.find(originEl => originEl.elId === el.elId)
|
||||
if(originElement && (originElement.type === ElementTypes.IMAGE || originElement.type === ElementTypes.SHAPE)) {
|
||||
newEl.width = originElement.width * widthScale
|
||||
newEl.height = originElement.height * heightScale
|
||||
newEl.left = currentMinX + (originElement.left - minX) * widthScale
|
||||
newEl.top = currentMinY + (originElement.top - minY) * heightScale
|
||||
}
|
||||
}
|
||||
return newEl
|
||||
})
|
||||
}
|
||||
|
||||
document.onmouseup = e => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
if(startPageX === e.pageX && startPageY === e.pageY) return
|
||||
|
||||
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scaleElement,
|
||||
scaleMultiElement,
|
||||
}
|
||||
}
|
97
src/views/Editor/Canvas/hooks/useSelectElement.ts
Normal file
97
src/views/Editor/Canvas/hooks/useSelectElement.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { Ref, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import uniq from 'lodash/uniq'
|
||||
import { State, MutationTypes } from '@/store'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
|
||||
export default (
|
||||
elementList: Ref<PPTElement[]>,
|
||||
activeElementIdList: Ref<string[]>,
|
||||
activeGroupElementId: Ref<string>,
|
||||
editorAreaFocus: Ref<boolean>,
|
||||
handleElementId: Ref<string>,
|
||||
moveElement: (e: MouseEvent, element: PPTElement) => void,
|
||||
) => {
|
||||
const store = useStore<State>()
|
||||
const ctrlOrShiftKeyActive = computed(() => store.getters.ctrlOrShiftKeyActive)
|
||||
|
||||
const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => {
|
||||
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
|
||||
|
||||
// 如果被点击的元素处于未激活状态,则将他设置为激活元素(单选),或者加入到激活元素中(多选)
|
||||
if(!activeElementIdList.value.includes(element.elId)) {
|
||||
let newActiveIdList: string[] = []
|
||||
|
||||
if(ctrlOrShiftKeyActive.value) {
|
||||
newActiveIdList = [...activeElementIdList.value, element.elId]
|
||||
}
|
||||
else newActiveIdList = [element.elId]
|
||||
|
||||
// 同时如果该元素是分组成员,需要将和他同组的元素一起激活
|
||||
if(element.groupId) {
|
||||
const groupMembersId: string[] = []
|
||||
elementList.value.forEach((el: PPTElement) => {
|
||||
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
|
||||
})
|
||||
newActiveIdList = [...newActiveIdList, ...groupMembersId]
|
||||
}
|
||||
|
||||
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, uniq(newActiveIdList))
|
||||
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
|
||||
}
|
||||
|
||||
// 如果被点击的元素已激活,且按下了多选按钮,则取消其激活状态(除非该元素或分组是最后的一个激活元素)
|
||||
else if(ctrlOrShiftKeyActive.value) {
|
||||
let newActiveIdList: string[] = []
|
||||
|
||||
// 同时如果该元素是分组成员,需要将和他同组的元素一起取消
|
||||
if(element.groupId) {
|
||||
const groupMembersId: string[] = []
|
||||
elementList.value.forEach((el: PPTElement) => {
|
||||
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
|
||||
})
|
||||
newActiveIdList = activeElementIdList.value.filter(elId => !groupMembersId.includes(elId))
|
||||
}
|
||||
else {
|
||||
newActiveIdList = activeElementIdList.value.filter(elId => elId !== element.elId)
|
||||
}
|
||||
|
||||
if(newActiveIdList.length > 0) {
|
||||
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveIdList)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果被点击的元素已激活,且没有按下多选按钮,且该元素不是当前操作元素,则将其设置为当前操作元素
|
||||
else if(handleElementId.value !== element.elId) {
|
||||
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
|
||||
}
|
||||
|
||||
else if(activeGroupElementId.value !== element.elId && element.groupId) {
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
|
||||
;(e.target as HTMLElement).onmouseup = (e: MouseEvent) => {
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
if(startPageX === currentPageX && startPageY === currentPageY) {
|
||||
activeGroupElementId.value = element.elId
|
||||
;(e.target as HTMLElement).onmouseup = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(canMove) moveElement(e, element)
|
||||
}
|
||||
|
||||
const selectAllElement = () => {
|
||||
const unlockedElements = elementList.value.filter(el => !el.isLock)
|
||||
const newActiveElementIdList = unlockedElements.map(el => el.elId)
|
||||
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
|
||||
}
|
||||
|
||||
return {
|
||||
selectElement,
|
||||
selectAllElement,
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
import { ref, reactive, Ref, toRefs } from 'vue'
|
||||
|
||||
export default (e: MouseEvent, viewportRef: Ref<HTMLElement | null>, canvasScale: number) => {
|
||||
const isMouseDown = ref(false)
|
||||
const mouseSelectionState = reactive({
|
||||
isShow: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
quadrant: 4,
|
||||
})
|
||||
const startMousePosition = reactive({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
const minSelectionRange = 5
|
||||
|
||||
if(!viewportRef.value) return
|
||||
|
||||
isMouseDown.value = true
|
||||
const viewportRect = viewportRef.value.getBoundingClientRect()
|
||||
|
||||
startMousePosition.x = e.pageX
|
||||
startMousePosition.y = e.pageY
|
||||
|
||||
mouseSelectionState.top = (startMousePosition.x - viewportRect.y) / canvasScale
|
||||
mouseSelectionState.left = (startMousePosition.y - viewportRect.x) / canvasScale
|
||||
|
||||
const mousemoveListener = (e: MouseEvent) => {
|
||||
if(!isMouseDown.value) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
const offsetWidth = (currentPageX - startMousePosition.x) / canvasScale
|
||||
const offsetHeight = (currentPageY - startMousePosition.y) / canvasScale
|
||||
|
||||
const width = Math.abs(offsetWidth)
|
||||
const height = Math.abs(offsetHeight)
|
||||
|
||||
if(width < minSelectionRange || height < minSelectionRange) return
|
||||
|
||||
let quadrant = 0
|
||||
if(offsetWidth > 0 && offsetHeight > 0) quadrant = 4
|
||||
else if(offsetWidth < 0 && offsetHeight < 0) quadrant = 1
|
||||
else if(offsetWidth > 0 && offsetHeight < 0) quadrant = 2
|
||||
else if(offsetWidth < 0 && offsetHeight > 0) quadrant = 3
|
||||
|
||||
mouseSelectionState.isShow = true
|
||||
mouseSelectionState.quadrant = quadrant
|
||||
mouseSelectionState.width = width
|
||||
mouseSelectionState.height = height
|
||||
}
|
||||
const mouseupListener = () => {
|
||||
isMouseDown.value = false
|
||||
mouseSelectionState.isShow = false
|
||||
|
||||
document.removeEventListener('mousemove', mousemoveListener)
|
||||
document.removeEventListener('mouseup', mouseupListener)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', mousemoveListener)
|
||||
document.addEventListener('mouseup', mouseupListener)
|
||||
|
||||
return { ...toRefs(mouseSelectionState) }
|
||||
}
|
File diff suppressed because it is too large
Load Diff
17
src/views/Editor/Canvas/types/index.ts
Normal file
17
src/views/Editor/Canvas/types/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface AlignmentLineAxis {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface AlignmentLineProps {
|
||||
type: 'vertical' | 'horizontal';
|
||||
axis: AlignmentLineAxis;
|
||||
length: number;
|
||||
}
|
||||
|
||||
export interface MultiSelectRange {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { PPTElement } from '@/types/slides'
|
||||
import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit'
|
||||
import { getElementListRange } from './elementRange'
|
||||
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||
|
||||
// 将元素对齐到屏幕
|
||||
export const alignElementToCanvas = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[], command: ElementAlignCommand) => {
|
||||
const viewportWidth = VIEWPORT_SIZE
|
||||
const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
|
||||
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList)
|
||||
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
for(const element of copyOfElementList) {
|
||||
if(!activeElementIdList.includes(element.elId)) continue
|
||||
|
||||
if(command === ElementAlignCommands.TOP) {
|
||||
const offsetY = minY - 0
|
||||
element.top = element.top - offsetY
|
||||
}
|
||||
else if(command === ElementAlignCommands.VERTICAL) {
|
||||
const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
|
||||
element.top = element.top - offsetY
|
||||
}
|
||||
else if(command === ElementAlignCommands.BOTTOM) {
|
||||
const offsetY = maxY - viewportHeight
|
||||
element.top = element.top - offsetY
|
||||
}
|
||||
|
||||
else if(command === ElementAlignCommands.LEFT) {
|
||||
const offsetX = minX - 0
|
||||
element.left = element.left - offsetX
|
||||
}
|
||||
else if(command === ElementAlignCommands.HORIZONTAL) {
|
||||
const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
|
||||
element.left = element.left - offsetX
|
||||
}
|
||||
else if(command === ElementAlignCommands.RIGHT) {
|
||||
const offsetX = maxX - viewportWidth
|
||||
element.left = element.left - offsetX
|
||||
}
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import { createRandomCode } from '@/utils/common'
|
||||
import { PPTElement } from '@/types/slides'
|
||||
|
||||
// 组合元素(为当前所有激活元素添加一个相同的groupId)
|
||||
export const combineElements = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[]) => {
|
||||
if(!activeElementList.length) return null
|
||||
|
||||
let copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
const groupId = createRandomCode()
|
||||
|
||||
const combineElementList: PPTElement[] = []
|
||||
for(const element of copyOfElementList) {
|
||||
if(activeElementIdList.includes(element.elId)) {
|
||||
element.groupId = groupId
|
||||
combineElementList.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
// 注意,组合元素的层级应该是连续的,所以需要获取该组元素中最顶层的元素,将组内其他成员从原位置移动到最顶层的元素的下面
|
||||
const combineElementMaxIndex = copyOfElementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
|
||||
const combineElementIdList = combineElementList.map(_element => _element.elId)
|
||||
copyOfElementList = copyOfElementList.filter(_element => !combineElementIdList.includes(_element.elId))
|
||||
|
||||
const insertIndex = combineElementMaxIndex - combineElementList.length + 1
|
||||
copyOfElementList.splice(insertIndex, 0, ...combineElementList)
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
// 取消组合元素(移除所有被激活元素的groupId)
|
||||
export const uncombineElements = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[]) => {
|
||||
if(!activeElementList.length) return null
|
||||
const hasElementInGroup = activeElementList.some(item => item.groupId)
|
||||
if(!hasElementInGroup) return null
|
||||
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
for(const element of copyOfElementList) {
|
||||
if(activeElementIdList.includes(element.elId) && element.groupId) delete element.groupId
|
||||
}
|
||||
return copyOfElementList
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { PPTElement } from '@/types/slides'
|
||||
|
||||
export const lockElement = (elementList: PPTElement[], handleElement: PPTElement, activeElementIdList: string[]) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
for(const element of copyOfElementList) {
|
||||
if(activeElementIdList.includes(handleElement.elId)) element.isLock = true
|
||||
}
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
export const unlockElement = (elementList: PPTElement[], handleElement: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
if(handleElement.groupId) {
|
||||
for(const element of copyOfElementList) {
|
||||
if(element.groupId === handleElement.groupId) element.isLock = false
|
||||
}
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
for(const element of copyOfElementList) {
|
||||
if(element.elId === handleElement.elId) {
|
||||
element.isLock = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return copyOfElementList
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
import { PPTElement } from '@/types/slides'
|
||||
import { ElementOrderCommand, ElementOrderCommands } from '@/types/edit'
|
||||
|
||||
// 获取组合元素层级范围(组合成员中的最大层级和最小层级)
|
||||
const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
|
||||
const minIndex = elementList.findIndex(_element => _element.elId === combineElementList[0].elId)
|
||||
const maxIndex = elementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
|
||||
return { minIndex, maxIndex }
|
||||
}
|
||||
|
||||
// 上移一层,返回移动后新的元素列表(下移一层逻辑类似)
|
||||
const moveUpElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
// 被操作的元素是组合元素成员
|
||||
if(element.groupId) {
|
||||
|
||||
// 获取该组合元素全部成员,以及组合元素层级范围
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(maxIndex === elementList.length - 1) return null
|
||||
|
||||
// 该组合元素上一层的元素,以下简称为【元素next】
|
||||
const nextElement = copyOfElementList[maxIndex + 1]
|
||||
|
||||
// 从元素列表中移除该组合元素全部成员
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
|
||||
// 如果【元素next】也是组合元素成员(另一个组合,不是上面被移除的那一组,以下简称为【组合next】)
|
||||
// 需要获取【组合next】全部成员的长度,将上面移除的组合元素全部成员添加到【组合next】全部成员的上方
|
||||
if(nextElement.groupId) {
|
||||
const nextCombineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
|
||||
copyOfElementList.splice(minIndex + nextCombineElementList.length, 0, ...movedElementList)
|
||||
}
|
||||
|
||||
// 如果【元素next】是单独的元素(非组合成员),将上面移除的组合元素全部成员添加到【元素next】上方
|
||||
else copyOfElementList.splice(minIndex + 1, 0, ...movedElementList)
|
||||
}
|
||||
|
||||
// 被操作的元素是单独的元素(非组合成员)
|
||||
else {
|
||||
|
||||
// 元素在元素列表中的层级
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(elementIndex === elementList.length - 1) return null
|
||||
|
||||
// 上一层的元素,以下简称为【元素next】
|
||||
const nextElement = copyOfElementList[elementIndex + 1]
|
||||
|
||||
// 从元素列表中移除被操作的元素
|
||||
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
|
||||
|
||||
// 如果【元素next】是组合元素成员
|
||||
// 需要获取该组合全部成员的长度,将上面移除的元素添加到该组合全部成员的上方
|
||||
if(nextElement.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
|
||||
copyOfElementList.splice(elementIndex + combineElementList.length, 0, movedElement)
|
||||
}
|
||||
|
||||
// 如果【元素next】是单独的元素(非组合成员),将上面移除的元素添加到【元素next】上方
|
||||
else copyOfElementList.splice(elementIndex + 1, 0, movedElement)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
// 下移一层
|
||||
const moveDownElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
if(element.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
if(minIndex === 0) return null
|
||||
const prevElement = copyOfElementList[minIndex - 1]
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
if(prevElement.groupId) {
|
||||
const prevCombineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
|
||||
copyOfElementList.splice(minIndex - prevCombineElementList.length, 0, ...movedElementList)
|
||||
}
|
||||
else copyOfElementList.splice(minIndex - 1, 0, ...movedElementList)
|
||||
}
|
||||
|
||||
else {
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
if(elementIndex === 0) return null
|
||||
const prevElement = copyOfElementList[elementIndex - 1]
|
||||
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
|
||||
if(prevElement.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
|
||||
copyOfElementList.splice(elementIndex - combineElementList.length, 0, movedElement)
|
||||
}
|
||||
else copyOfElementList.splice(elementIndex - 1, 0, movedElement)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
// 置顶层,返回移动后新的元素列表(置底层逻辑类似)
|
||||
const moveTopElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
// 被操作的元素是组合元素成员
|
||||
if(element.groupId) {
|
||||
|
||||
// 获取该组合元素全部成员,以及组合元素层级范围
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(maxIndex === elementList.length - 1) return null
|
||||
|
||||
// 从元素列表中移除该组合元素全部成员,然后添加到元素列表最上方
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
copyOfElementList.push(...movedElementList)
|
||||
}
|
||||
|
||||
// 被操作的元素是单独的元素(非组合成员)
|
||||
else {
|
||||
|
||||
// 元素在元素列表中的层级
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
|
||||
// 无法移动(已经处在顶层)
|
||||
if(elementIndex === elementList.length - 1) return null
|
||||
|
||||
// 从元素列表中移除该元素,然后添加到元素列表最上方
|
||||
copyOfElementList.splice(elementIndex, 1)
|
||||
copyOfElementList.push(element)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
// 置底层
|
||||
const moveBottomElement = (elementList: PPTElement[], element: PPTElement) => {
|
||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
||||
|
||||
if(element.groupId) {
|
||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
||||
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
|
||||
if(minIndex === 0) return null
|
||||
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
|
||||
copyOfElementList.unshift(...movedElementList)
|
||||
}
|
||||
|
||||
else {
|
||||
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
|
||||
if(elementIndex === 0) return null
|
||||
copyOfElementList.splice(elementIndex, 1)
|
||||
copyOfElementList.unshift(element)
|
||||
}
|
||||
|
||||
return copyOfElementList
|
||||
}
|
||||
|
||||
export const orderElement = (elementList: PPTElement[], element: PPTElement, command: ElementOrderCommand) => {
|
||||
let newElementList = null
|
||||
|
||||
if(command === ElementOrderCommands.UP) newElementList = moveUpElement(elementList, element)
|
||||
else if(command === ElementOrderCommands.DOWN) newElementList = moveDownElement(elementList, element)
|
||||
else if(command === ElementOrderCommands.TOP) newElementList = moveTopElement(elementList, element)
|
||||
else if(command === ElementOrderCommands.BOTTOM) newElementList = moveBottomElement(elementList, element)
|
||||
|
||||
return newElementList
|
||||
}
|
@ -203,7 +203,7 @@ export default defineComponent({
|
||||
text: '删除',
|
||||
subText: 'Delete',
|
||||
icon: 'icon-delete',
|
||||
handler: () => props.deleteElement(),
|
||||
handler: props.deleteElement,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user